diff --git a/AutomatedTesting/Gem/PythonTests/AWS/README.md b/AutomatedTesting/Gem/PythonTests/AWS/README.md index 1bb36f178d..0d046cbe4c 100644 --- a/AutomatedTesting/Gem/PythonTests/AWS/README.md +++ b/AutomatedTesting/Gem/PythonTests/AWS/README.md @@ -8,11 +8,14 @@ ## Deploy CDK Applications 1. Go to the AWS IAM console and create an IAM role called o3de-automation-tests which adds your own account as as a trusted entity and uses the "AdministratorAccess" permissions policy. 2. Copy {engine_root}\scripts\build\Platform\Windows\deploy_cdk_applications.cmd to your engine root folder. -3. Open a new Command Prompt window at the engine root and set the following environment variables: +3. Open a new Command Prompt window at the engine root and set the following environment variables: +``` Set O3DE_AWS_PROJECT_NAME=AWSAUTO Set O3DE_AWS_DEPLOY_REGION=us-east-1 + Set O3DE_AWS_DEPLOY_ACCOUNT={your_aws_account_id} Set ASSUME_ROLE_ARN=arn:aws:iam::{your_aws_account_id}:role/o3de-automation-tests Set COMMIT_ID=HEAD +``` 4. In the same Command Prompt window, Deploy the CDK applications for AWS gems by running deploy_cdk_applications.cmd. ## Run Automation Tests diff --git a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main.py b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main.py index 3403c938a8..6cc48984ab 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main.py @@ -253,73 +253,3 @@ class TestAtomEditorComponentsMain(object): ) -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("launcher_platform", ['windows_generic']) -@pytest.mark.system -class TestMaterialEditorBasicTests(object): - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project): - def delete_files(): - file_system.delete( - [ - os.path.join(workspace.paths.project(), "Materials", "test_material.material"), - os.path.join(workspace.paths.project(), "Materials", "test_material_1.material"), - os.path.join(workspace.paths.project(), "Materials", "test_material_2.material"), - ], - True, - True, - ) - # Cleanup our newly created materials - delete_files() - - def teardown(): - # Cleanup our newly created materials - delete_files() - - request.addfinalizer(teardown) - - @pytest.mark.parametrize("exe_file_name", ["MaterialEditor"]) - @pytest.mark.test_case_id("C34448113") # Creating a New Asset. - @pytest.mark.test_case_id("C34448114") # Opening an Existing Asset. - @pytest.mark.test_case_id("C34448115") # Closing Selected Material. - @pytest.mark.test_case_id("C34448116") # Closing All Materials. - @pytest.mark.test_case_id("C34448117") # Closing all but Selected Material. - @pytest.mark.test_case_id("C34448118") # Saving Material. - @pytest.mark.test_case_id("C34448119") # Saving as a New Material. - @pytest.mark.test_case_id("C34448120") # Saving as a Child Material. - @pytest.mark.test_case_id("C34448121") # Saving all Open Materials. - def test_MaterialEditorBasicTests( - self, request, workspace, project, launcher_platform, generic_launcher, exe_file_name): - - expected_lines = [ - "Material opened: True", - "Test asset doesn't exist initially: True", - "New asset created: True", - "New Material opened: True", - "Material closed: True", - "All documents closed: True", - "Close All Except Selected worked as expected: True", - "Actual Document saved with changes: True", - "Document saved as copy is saved with changes: True", - "Document saved as child is saved with changes: True", - "Save All worked as expected: True", - ] - unexpected_lines = [ - # "Trace::Assert", - # "Trace::Error", - "Traceback (most recent call last):" - ] - - hydra.launch_and_validate_results( - request, - TEST_DIRECTORY, - generic_launcher, - "hydra_AtomMaterialEditor_BasicTests.py", - run_python="--runpython", - timeout=120, - expected_lines=expected_lines, - unexpected_lines=unexpected_lines, - halt_on_unexpected=True, - null_renderer=True, - log_file_name="MaterialEditor.log", - ) diff --git a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_GPU.py b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_GPU.py index 381f266fab..23fb249761 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_GPU.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_GPU.py @@ -89,7 +89,7 @@ class TestAllComponentsIndepthTests(object): level_creation_expected_lines = [ "Viewport is set to the expected size: True", - "Basic level created" + "Exited game mode" ] unexpected_lines = [ "Trace::Assert", @@ -189,8 +189,8 @@ class TestPerformanceBenchmarkSuite(object): "Benchmark metadata captured.", "Pass timestamps captured.", "CPU frame time captured.", - "Capturing complete.", - "Captured data successfully." + "Captured data successfully.", + "Exited game mode" ] unexpected_lines = [ diff --git a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py index 58e5d00ff2..ad45e51080 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py @@ -14,10 +14,6 @@ import editor_python_test_tools.hydra_test_utils as hydra logger = logging.getLogger(__name__) TEST_DIRECTORY = os.path.join(os.path.dirname(__file__), "tests") - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -@pytest.mark.parametrize("level", ["auto_test"]) class TestAtomEditorComponentsSandbox(object): # It requires at least one test @@ -70,3 +66,75 @@ class TestAtomEditorComponentsSandbox(object): null_renderer=True, cfg_args=cfg_args, ) + +@pytest.mark.parametrize("project", ["AutomatedTesting"]) +@pytest.mark.parametrize("launcher_platform", ['windows_generic']) +@pytest.mark.system +class TestMaterialEditorBasicTests(object): + @pytest.fixture(autouse=True) + def setup_teardown(self, request, workspace, project): + def delete_files(): + file_system.delete( + [ + os.path.join(workspace.paths.project(), "Materials", "test_material.material"), + os.path.join(workspace.paths.project(), "Materials", "test_material_1.material"), + os.path.join(workspace.paths.project(), "Materials", "test_material_2.material"), + ], + True, + True, + ) + # Cleanup our newly created materials + delete_files() + + def teardown(): + # Cleanup our newly created materials + delete_files() + + request.addfinalizer(teardown) + + @pytest.mark.parametrize("exe_file_name", ["MaterialEditor"]) + @pytest.mark.test_case_id("C34448113") # Creating a New Asset. + @pytest.mark.test_case_id("C34448114") # Opening an Existing Asset. + @pytest.mark.test_case_id("C34448115") # Closing Selected Material. + @pytest.mark.test_case_id("C34448116") # Closing All Materials. + @pytest.mark.test_case_id("C34448117") # Closing all but Selected Material. + @pytest.mark.test_case_id("C34448118") # Saving Material. + @pytest.mark.test_case_id("C34448119") # Saving as a New Material. + @pytest.mark.test_case_id("C34448120") # Saving as a Child Material. + @pytest.mark.test_case_id("C34448121") # Saving all Open Materials. + def test_MaterialEditorBasicTests( + self, request, workspace, project, launcher_platform, generic_launcher, exe_file_name): + + expected_lines = [ + "Material opened: True", + "Test asset doesn't exist initially: True", + "New asset created: True", + "New Material opened: True", + "Material closed: True", + "All documents closed: True", + "Close All Except Selected worked as expected: True", + "Actual Document saved with changes: True", + "Document saved as copy is saved with changes: True", + "Document saved as child is saved with changes: True", + "Save All worked as expected: True", + ] + unexpected_lines = [ + # "Trace::Assert", + # "Trace::Error", + "Traceback (most recent call last):" + ] + + hydra.launch_and_validate_results( + request, + TEST_DIRECTORY, + generic_launcher, + "hydra_AtomMaterialEditor_BasicTests.py", + run_python="--runpython", + timeout=120, + expected_lines=expected_lines, + unexpected_lines=unexpected_lines, + halt_on_unexpected=True, + null_renderer=True, + log_file_name="MaterialEditor.log", + ) + diff --git a/AutomatedTesting/Gem/PythonTests/Atom/atom_utils/atom_constants.py b/AutomatedTesting/Gem/PythonTests/Atom/atom_utils/atom_constants.py index 88a959f198..344a0dbd29 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/atom_utils/atom_constants.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/atom_utils/atom_constants.py @@ -17,3 +17,392 @@ LIGHT_TYPES = { 'simple_point': 6, 'simple_spot': 7, } + + +class AtomComponentProperties: + """ + Holds Atom component related constants + """ + + @staticmethod + def actor(property: str = 'name') -> str: + """ + Actor component properties. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Actor', + } + return properties[property] + + @staticmethod + def bloom(property: str = 'name') -> str: + """ + Bloom component properties. Requires PostFX Layer component. + - 'requires' a list of component names as strings required by this component. + Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Bloom', + 'requires': [AtomComponentProperties.postfx_layer()], + } + return properties[property] + + @staticmethod + def camera(property: str = 'name') -> str: + """ + Camera component properties. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Camera', + } + return properties[property] + + @staticmethod + def decal(property: str = 'name') -> str: + """ + Decal component properties. + - 'Material' the material Asset.id of the decal. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Decal', + 'Material': 'Controller|Configuration|Material', + } + return properties[property] + + @staticmethod + def deferred_fog(property: str = 'name') -> str: + """ + Deferred Fog component properties. Requires PostFX Layer component. + - 'requires' a list of component names as strings required by this component. + Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Deferred Fog', + 'requires': [AtomComponentProperties.postfx_layer()], + } + return properties[property] + + @staticmethod + def depth_of_field(property: str = 'name') -> str: + """ + Depth of Field component properties. Requires PostFX Layer component. + - 'requires' a list of component names as strings required by this component. + Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n + - 'Camera Entity' an EditorEntity.id reference to the Camera component required for this effect. + Must be a different entity than the one which hosts Depth of Field component.\n + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'DepthOfField', + 'requires': [AtomComponentProperties.postfx_layer()], + 'Camera Entity': 'Controller|Configuration|Camera Entity', + } + return properties[property] + + @staticmethod + def diffuse_probe(property: str = 'name') -> str: + """ + Diffuse Probe Grid component properties. Requires one of 'shapes'. + - 'shapes' a list of supported shapes as component names. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Diffuse Probe Grid', + 'shapes': ['Axis Aligned Box Shape', 'Box Shape'] + } + return properties[property] + + @staticmethod + def directional_light(property: str = 'name') -> str: + """ + Directional Light component properties. + - 'Camera' an EditorEntity.id reference to the Camera component that controls cascaded shadow view frustum. + Must be a different entity than the one which hosts Directional Light component.\n + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Directional Light', + 'Camera': 'Controller|Configuration|Shadow|Camera', + } + return properties[property] + + @staticmethod + def display_mapper(property: str = 'name') -> str: + """ + Display Mapper component properties. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Display Mapper', + } + return properties[property] + + @staticmethod + def entity_reference(property: str = 'name') -> str: + """ + Entity Reference component properties. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Entity Reference', + } + return properties[property] + + @staticmethod + def exposure_control(property: str = 'name') -> str: + """ + Exposure Control component properties. Requires PostFX Layer component. + - 'requires' a list of component names as strings required by this component. + Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Exposure Control', + 'requires': [AtomComponentProperties.postfx_layer()], + } + return properties[property] + + @staticmethod + def global_skylight(property: str = 'name') -> str: + """ + Global Skylight (IBL) component properties. + - 'Diffuse Image' Asset.id for the cubemap image for determining diffuse lighting. + - 'Specular Image' Asset.id for the cubemap image for determining specular lighting. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Global Skylight (IBL)', + 'Diffuse Image': 'Controller|Configuration|Diffuse Image', + 'Specular Image': 'Controller|Configuration|Specular Image', + } + return properties[property] + + @staticmethod + def grid(property: str = 'name') -> str: + """ + Grid component properties. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Grid', + } + return properties[property] + + @staticmethod + def hdr_color_grading(property: str = 'name') -> str: + """ + HDR Color Grading component properties. Requires PostFX Layer component. + - 'requires' a list of component names as strings required by this component. + Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'HDR Color Grading', + 'requires': [AtomComponentProperties.postfx_layer()], + } + return properties[property] + + @staticmethod + def hdri_skybox(property: str = 'name') -> str: + """ + HDRi Skybox component properties. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'HDRi Skybox', + } + return properties[property] + + @staticmethod + def light(property: str = 'name') -> str: + """ + Light component properties. + - 'Light type' from atom_constants.py LIGHT_TYPES + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Light', + 'Light type': 'Controller|Configuration|Light type', + } + return properties[property] + + @staticmethod + def look_modification(property: str = 'name') -> str: + """ + Look Modification component properties. Requires PostFX Layer component. + - 'requires' a list of component names as strings required by this component. + Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Look Modification', + 'requires': [AtomComponentProperties.postfx_layer()], + } + return properties[property] + + @staticmethod + def material(property: str = 'name') -> str: + """ + Material component properties. Requires one of Actor OR Mesh component. + - 'requires' a list of component names as strings required by this component. + Only one of these is required at a time for this component.\n + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Material', + 'requires': [AtomComponentProperties.actor(), AtomComponentProperties.mesh()], + } + return properties[property] + + @staticmethod + def mesh(property: str = 'name') -> str: + """ + Mesh component properties. + - 'Mesh Asset' Asset.id of the mesh model. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + :rtype: str + """ + properties = { + 'name': 'Mesh', + 'Mesh Asset': 'Controller|Configuration|Mesh Asset', + } + return properties[property] + + @staticmethod + def occlusion_culling_plane(property: str = 'name') -> str: + """ + Occlusion Culling Plane component properties. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Occlusion Culling Plane', + } + return properties[property] + + @staticmethod + def physical_sky(property: str = 'name') -> str: + """ + Physical Sky component properties. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Physical Sky', + } + return properties[property] + + @staticmethod + def postfx_layer(property: str = 'name') -> str: + """ + PostFX Layer component properties. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'PostFX Layer', + } + return properties[property] + + @staticmethod + def postfx_gradient(property: str = 'name') -> str: + """ + PostFX Gradient Weight Modifier component properties. Requires PostFX Layer component. + - 'requires' a list of component names as strings required by this component. + Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'PostFX Gradient Weight Modifier', + 'requires': [AtomComponentProperties.postfx_layer()], + } + return properties[property] + + @staticmethod + def postfx_radius(property: str = 'name') -> str: + """ + PostFX Radius Weight Modifier component properties. Requires PostFX Layer component. + - 'requires' a list of component names as strings required by this component. + Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'PostFX Radius Weight Modifier', + 'requires': [AtomComponentProperties.postfx_layer()], + } + return properties[property] + + @staticmethod + def postfx_shape(property: str = 'name') -> str: + """ + PostFX Shape Weight Modifier component properties. Requires PostFX Layer and one of 'shapes' listed. + - 'requires' a list of component names as strings required by this component. + Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n + - 'shapes' a list of supported shapes as component names. 'Tube Shape' is also supported but requires 'Spline'. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'PostFX Shape Weight Modifier', + 'requires': [AtomComponentProperties.postfx_layer()], + 'shapes': ['Axis Aligned Box Shape', 'Box Shape', 'Capsule Shape', 'Compound Shape', 'Cylinder Shape', + 'Disk Shape', 'Polygon Prism Shape', 'Quad Shape', 'Sphere Shape', 'Vegetation Reference Shape'], + } + return properties[property] + + @staticmethod + def reflection_probe(property: str = 'name') -> str: + """ + Reflection Probe component properties. Requires one of 'shapes' listed. + - 'shapes' a list of supported shapes as component names. + - 'Baked Cubemap Path' Asset.id of the baked cubemap image generated by a call to 'BakeReflectionProbe' ebus. + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'Reflection Probe', + 'shapes': ['Axis Aligned Box Shape', 'Box Shape'], + 'Baked Cubemap Path': 'Cubemap|Baked Cubemap Path', + } + return properties[property] + + @staticmethod + def ssao(property: str = 'name') -> str: + """ + SSAO component properties. Requires PostFX Layer component. + - 'requires' a list of component names as strings required by this component. + Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n + :param property: From the last element of the property tree path. Default 'name' for component name string. + :return: Full property path OR component name if no property specified. + """ + properties = { + 'name': 'SSAO', + 'requires': [AtomComponentProperties.postfx_layer()], + } + return properties[property] diff --git a/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_MeshAdded.py b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_MeshAdded.py index fbf5c987d1..82d3b89309 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_MeshAdded.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_MeshAdded.py @@ -80,25 +80,25 @@ def AtomEditorComponents_Mesh_AddedToEntity(): from editor_python_test_tools.asset_utils import Asset from editor_python_test_tools.editor_entity_utils import EditorEntity - from editor_python_test_tools.utils import Report, Tracer, TestHelper as helper + from editor_python_test_tools.utils import Report, Tracer, TestHelper + from Atom.atom_utils.atom_constants import AtomComponentProperties as Atom with Tracer() as error_tracer: # Test setup begins. # Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level. - helper.init_idle() - helper.open_level("", "Base") + TestHelper.init_idle() + TestHelper.open_level("", "Base") # Test steps begin. # 1. Create a Mesh entity with no components. - mesh_name = "Mesh" - mesh_entity = EditorEntity.create_editor_entity(mesh_name) + mesh_entity = EditorEntity.create_editor_entity(Atom.mesh()) Report.critical_result(Tests.mesh_entity_creation, mesh_entity.exists()) # 2. Add a Mesh component to Mesh entity. - mesh_component = mesh_entity.add_component(mesh_name) + mesh_component = mesh_entity.add_component(Atom.mesh()) Report.critical_result( Tests.mesh_component_added, - mesh_entity.has_component(mesh_name)) + mesh_entity.has_component(Atom.mesh())) # 3. UNDO the entity creation and component addition. # -> UNDO component addition. @@ -125,17 +125,16 @@ def AtomEditorComponents_Mesh_AddedToEntity(): Report.result(Tests.creation_redo, mesh_entity.exists()) # 5. Set Mesh component asset property - mesh_property_asset = 'Controller|Configuration|Mesh Asset' model_path = os.path.join('Objects', 'shaderball', 'shaderball_default_1m.azmodel') model = Asset.find_asset_by_path(model_path) - mesh_component.set_component_property_value(mesh_property_asset, model.id) + mesh_component.set_component_property_value(Atom.mesh('Mesh Asset'), model.id) Report.result(Tests.mesh_asset_specified, - mesh_component.get_component_property_value(mesh_property_asset) == model.id) + mesh_component.get_component_property_value(Atom.mesh('Mesh Asset')) == model.id) # 6. Enter/Exit game mode. - helper.enter_game_mode(Tests.enter_game_mode) + TestHelper.enter_game_mode(Tests.enter_game_mode) general.idle_wait_frames(1) - helper.exit_game_mode(Tests.exit_game_mode) + TestHelper.exit_game_mode(Tests.exit_game_mode) # 7. Test IsHidden. mesh_entity.set_visibility_state(False) @@ -159,7 +158,7 @@ def AtomEditorComponents_Mesh_AddedToEntity(): Report.result(Tests.deletion_redo, not mesh_entity.exists()) # 12. Look for errors or asserts. - helper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0) + TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0) for error_info in error_tracer.errors: Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}") for assert_info in error_tracer.asserts: diff --git a/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_AtomFeatureIntegrationBenchmark.py b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_AtomFeatureIntegrationBenchmark.py index fbd9f3459a..de7c9d2326 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_AtomFeatureIntegrationBenchmark.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_AtomFeatureIntegrationBenchmark.py @@ -90,7 +90,6 @@ def run(): benchmarker.capture_cpu_frame_time(i) general.exit_game_mode() helper.wait_for_condition(function=lambda: not general.is_in_game_mode(), timeout_in_seconds=2.0) - general.log("Capturing complete.") if __name__ == "__main__": diff --git a/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_BasicLevelSetup.py b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_BasicLevelSetup.py index 93e78a7c0a..9515712583 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_BasicLevelSetup.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_BasicLevelSetup.py @@ -215,7 +215,6 @@ def run(): ScreenshotHelper(general.idle_wait_frames).capture_screenshot_blocking(f"{'AtomBasicLevelSetup'}.ppm") general.exit_game_mode() helper.wait_for_condition(function=lambda: not general.is_in_game_mode(), timeout_in_seconds=2.0) - general.log("Basic level created") if __name__ == "__main__": diff --git a/AutomatedTesting/Gem/PythonTests/CMakeLists.txt b/AutomatedTesting/Gem/PythonTests/CMakeLists.txt index 8bccfb07b6..fa13aee9d0 100644 --- a/AutomatedTesting/Gem/PythonTests/CMakeLists.txt +++ b/AutomatedTesting/Gem/PythonTests/CMakeLists.txt @@ -33,7 +33,7 @@ add_subdirectory(WhiteBox) add_subdirectory(NvCloth) ## Prefab ## -add_subdirectory(prefab) +add_subdirectory(Prefab) ## Editor Python Bindings ## add_subdirectory(EditorPythonBindings) @@ -61,3 +61,6 @@ add_subdirectory(AWS) ## Multiplayer ## add_subdirectory(Multiplayer) + +## Integration tests for editor testing framework ## +add_subdirectory(editor_test_testing) \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py index 783f71e06c..844f8de903 100644 --- a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py @@ -122,17 +122,32 @@ class EditorEntity: # Creation functions @classmethod - def find_editor_entity(cls, entity_name: str) -> EditorEntity: + def find_editor_entity(cls, entity_name: str, must_be_unique : bool = False) -> EditorEntity: """ Given Entity name, outputs entity object :param entity_name: Name of entity to find :return: EditorEntity class object """ - entity_id = general.find_editor_entity(entity_name) - assert entity_id.IsValid(), f"Failure: Couldn't find entity with name: '{entity_name}'" - entity = cls(entity_id) + entities = cls.find_editor_entities([entity_name]) + assert len(entities) != 0, f"Failure: Couldn't find entity with name: '{entity_name}'" + if must_be_unique: + assert len(entities) == 1, f"Failure: Multiple entities with name: '{entity_name}' when expected only one" + + entity = cls(entities[0]) return entity + @classmethod + def find_editor_entities(cls, entity_names: List[str]) -> EditorEntity: + """ + Given Entities names, returns a list of EditorEntity + :param entity_name: Name of entity to find + :return: List[EditorEntity] class object + """ + searchFilter = azlmbr.entity.SearchFilter() + searchFilter.names = entity_names + ids = azlmbr.entity.SearchBus(bus.Broadcast, 'SearchEntities', searchFilter) + return [cls(id) for id in ids] + @classmethod def create_editor_entity(cls, name: str = None, parent_id=None) -> EditorEntity: """ @@ -157,8 +172,7 @@ class EditorEntity: cls, entity_position: Union[List, Tuple, math.Vector3], name: str = None, - parent_id: azlmbr.entity.EntityId = None, - ) -> EditorEntity: + parent_id: azlmbr.entity.EntityId = None) -> EditorEntity: """ Used to create entity at position using 'CreateNewEntityAtPosition' Bus. :param entity_position: World Position(X, Y, Z) of entity in viewport. @@ -227,6 +241,12 @@ class EditorEntity: """ return editor.EditorEntityInfoRequestBus(bus.Event, "GetChildren", self.id) + def get_children(self) -> List[EditorEntity]: + """ + :return: List of EditorEntity children. Type: [EditorEntity] + """ + return [EditorEntity(child_id) for child_id in self.get_children_ids()] + def add_component(self, component_name: str) -> EditorComponent: """ Used to add new component to Entity. diff --git a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py new file mode 100644 index 0000000000..10a6ab1ef4 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py @@ -0,0 +1,353 @@ +""" +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 +""" + +from __future__ import annotations +from collections import Counter +from collections import deque +from os import path +from pathlib import Path + +from PySide2 import QtWidgets + +import azlmbr.legacy.general as general +from azlmbr.entity import EntityId +from azlmbr.math import Vector3 +from editor_python_test_tools.editor_entity_utils import EditorEntity +from editor_python_test_tools.utils import Report + +import azlmbr.entity as entity +import azlmbr.bus as bus +import azlmbr.components as components +import azlmbr.editor as editor +import azlmbr.globals +import azlmbr.math as math +import azlmbr.prefab as prefab +import editor_python_test_tools.pyside_utils as pyside_utils + + +def get_prefab_file_path(prefab_path): + if not path.isabs(prefab_path): + prefab_path = path.join(general.get_file_alias("@projectroot@"), prefab_path) + + # Append prefab if it doesn't contain .prefab on it + name, ext = path.splitext(prefab_path) + if ext != ".prefab": + prefab_path = name + ".prefab" + return prefab_path + + +def get_all_entity_ids(): + return entity.SearchBus(bus.Broadcast, 'SearchEntities', entity.SearchFilter()) + +def wait_for_propagation(): + general.idle_wait_frames(1) + +# This is a helper class which contains some of the useful information about a prefab instance. +class PrefabInstance: + + def __init__(self, prefab_file_name: str = None, container_entity: EditorEntity = None): + self.prefab_file_name: str = prefab_file_name + self.container_entity: EditorEntity = container_entity + + def __eq__(self, other): + return other and self.container_entity.id == other.container_entity.id + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.container_entity.id) + + def is_valid(self) -> bool: + """ + See if this instance is valid to be used with other prefab operations. + :return: Whether the target instance is valid or not. + """ + return self.container_entity.id.IsValid() and self.prefab_file_name in Prefab.existing_prefabs + + def has_editor_prefab_component(self) -> bool: + """ + Check if the instance's container entity contains EditorPrefabComponent. + :return: Whether the container entity of target instance has EditorPrefabComponent in it or not. + """ + return editor.EditorComponentAPIBus(bus.Broadcast, "HasComponentOfType", self.container_entity.id, azlmbr.globals.property.EditorPrefabComponentTypeId) + + def is_at_position(self, expected_position): + """ + Check if the instance's container entity is at expected position given. + :return: Whether the container entity of target instance is at expected position or not. + """ + actual_position = components.TransformBus(bus.Event, "GetWorldTranslation", self.container_entity.id) + is_at_position = actual_position.IsClose(expected_position) + + if not is_at_position: + Report.info(f"Prefab Instance Container Entity '{self.container_entity.id.ToString()}'\'s expected position: {expected_position.ToString()}, actual position: {actual_position.ToString()}") + + return is_at_position + + async def ui_reparent_prefab_instance(self, parent_entity_id: EntityId): + """ + Reparent this instance to target parent entity. + The function will also check pop up dialog ui in editor to see if there's prefab cyclical dependency error while reparenting prefabs. + :param parent_entity_id: The id of the entity this instance should be a child of in the transform hierarchy next. + """ + container_entity_id_before_reparent = self.container_entity.id + + original_parent = EditorEntity(self.container_entity.get_parent_id()) + original_parent_before_reparent_children_ids = {child_id.ToString(): child_id for child_id in original_parent.get_children_ids()} + + new_parent = EditorEntity(parent_entity_id) + new_parent_before_reparent_children_ids = {child_id.ToString(): child_id for child_id in new_parent.get_children_ids()} + + pyside_utils.run_soon(lambda: self.container_entity.set_parent_entity(parent_entity_id)) + pyside_utils.run_soon(lambda: wait_for_propagation()) + + try: + active_modal_widget = await pyside_utils.wait_for_modal_widget() + error_message_box = active_modal_widget.findChild(QtWidgets.QMessageBox) + ok_button = error_message_box.button(QtWidgets.QMessageBox.Ok) + ok_button.click() + assert False, "Cyclical dependency detected while reparenting prefab" + except pyside_utils.EventLoopTimeoutException: + pass + + original_parent_after_reparent_children_ids = {child_id.ToString(): child_id for child_id in original_parent.get_children_ids()} + assert len(original_parent_after_reparent_children_ids) == len(original_parent_before_reparent_children_ids) - 1, \ + "The children count of the Prefab Instance's original parent should be decreased by 1." + assert not container_entity_id_before_reparent in original_parent_after_reparent_children_ids, \ + "This Prefab Instance is still a child entity of its original parent entity." + + new_parent_after_reparent_children_ids = {child_id.ToString(): child_id for child_id in new_parent.get_children_ids()} + assert len(new_parent_after_reparent_children_ids) == len(new_parent_before_reparent_children_ids) + 1, \ + "The children count of the Prefab Instance's new parent should be increased by 1." + + after_before_diff = set(new_parent_after_reparent_children_ids.keys()).difference(set(new_parent_before_reparent_children_ids.keys())) + container_entity_id_after_reparent = new_parent_after_reparent_children_ids[after_before_diff.pop()] + reparented_container_entity = EditorEntity(container_entity_id_after_reparent) + reparented_container_entity_parent_id = reparented_container_entity.get_parent_id() + has_correct_parent = reparented_container_entity_parent_id.ToString() == parent_entity_id.ToString() + assert has_correct_parent, "Prefab Instance reparented is *not* under the expected parent entity" + + current_instance_prefab = Prefab.get_prefab(self.prefab_file_name) + current_instance_prefab.instances.remove(self) + + self.container_entity = reparented_container_entity + current_instance_prefab.instances.add(self) + +# This is a helper class which contains some of the useful information about a prefab template. +class Prefab: + + existing_prefabs = {} + + def __init__(self, file_path: str): + self.file_path: str = get_prefab_file_path(file_path) + self.instances: set[PrefabInstance] = set() + + @classmethod + def is_prefab_loaded(cls, file_path: str) -> bool: + """ + Check if a prefab is ready to be used to generate its instances. + :param file_path: A unique file path of the target prefab. + :return: Whether the target prefab is loaded or not. + """ + return file_path in Prefab.existing_prefabs + + + @classmethod + def prefab_exists(cls, file_path: str) -> bool: + """ + Check if a prefab exists in the directory for files of prefab tests. + :param file_name: A unique file name of the target prefab. + :return: Whether the target prefab exists or not. + """ + return path.exists(get_prefab_file_path(file_path)) + + @classmethod + def get_prefab(cls, file_name: str) -> Prefab: + """ + Return a prefab which can be used immediately. + :param file_name: A unique file name of the target prefab. + :return: The prefab with given file name. + """ + assert file_name, "Received an empty file_name" + if Prefab.is_prefab_loaded(file_name): + return Prefab.existing_prefabs[file_name] + else: + assert Prefab.prefab_exists(file_name), f"Attempted to get a prefab \"{file_name}\" that doesn't exist" + new_prefab = Prefab(file_name) + Prefab.existing_prefabs[file_name] = Prefab(file_name) + return new_prefab + + @classmethod + def create_prefab(cls, entities: list[EditorEntity], file_name: str, prefab_instance_name: str=None) -> tuple(Prefab, PrefabInstance): + """ + Create a prefab in memory and return it. The very first instance of this prefab will also be created. + :param entities: The entities that should form the new prefab (along with their descendants). + :param file_name: A unique file name of new prefab. + :param prefab_instance_name: A name for the very first instance generated while prefab creation. The default instance name is the same as file_name. + :return: Created Prefab object and the very first PrefabInstance object owned by the prefab. + """ + assert not Prefab.is_prefab_loaded(file_name), f"Can't create Prefab '{file_name}' since the prefab already exists" + + new_prefab = Prefab(file_name) + entity_ids = [entity.id for entity in entities] + create_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'CreatePrefabInMemory', entity_ids, new_prefab.file_path) + assert create_prefab_result.IsSuccess(), f"Prefab operation 'CreatePrefab' failed. Error: {create_prefab_result.GetError()}" + + container_entity_id = create_prefab_result.GetValue() + container_entity = EditorEntity(container_entity_id) + children_entity_ids = container_entity.get_children_ids() + + assert len(children_entity_ids) == len(entities), f"Entity count of created prefab instance does *not* match the count of given entities." + + if prefab_instance_name: + container_entity.set_name(prefab_instance_name) + + wait_for_propagation() + + new_prefab_instance = PrefabInstance(file_name, EditorEntity(container_entity_id)) + new_prefab.instances.add(new_prefab_instance) + Prefab.existing_prefabs[file_name] = new_prefab + return new_prefab, new_prefab_instance + + @classmethod + def remove_prefabs(cls, prefab_instances: list[PrefabInstance]): + """ + Remove target prefab instances. + :param prefab_instances: Instances to be removed. + """ + entity_ids_to_remove = [] + entity_id_queue = [prefab_instance.container_entity for prefab_instance in prefab_instances] + while entity_id_queue: + entity = entity_id_queue.pop(0) + children_entity_ids = entity.get_children_ids() + for child_entity_id in children_entity_ids: + entity_id_queue.append(EditorEntity(child_entity_id)) + + entity_ids_to_remove.append(entity.id) + + container_entity_ids = [prefab_instance.container_entity.id for prefab_instance in prefab_instances] + delete_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DeleteEntitiesAndAllDescendantsInInstance', container_entity_ids) + assert delete_prefab_result.IsSuccess(), f"Prefab operation 'DeleteEntitiesAndAllDescendantsInInstance' failed. Error: {delete_prefab_result.GetError()}" + + wait_for_propagation() + + entity_ids_after_delete = set(get_all_entity_ids()) + + for entity_id_removed in entity_ids_to_remove: + if entity_id_removed in entity_ids_after_delete: + assert False, "Not all entities and descendants in target prefabs are deleted." + + for instance in prefab_instances: + instance_deleted_prefab = Prefab.get_prefab(instance.prefab_file_name) + instance_deleted_prefab.instances.remove(instance) + instance = PrefabInstance() + + @classmethod + def duplicate_prefabs(cls, prefab_instances: list[PrefabInstance]): + """ + Duplicate target prefab instances. + :param prefab_instances: Instances to be duplicated. + :return: PrefabInstance objects of given prefab instances' duplicates. + """ + assert prefab_instances, "Input list of prefab instances should *not* be empty." + + common_parent = EditorEntity(prefab_instances[0].container_entity.get_parent_id()) + common_parent_children_ids_before_duplicate = set([child_id.ToString() for child_id in common_parent.get_children_ids()]) + + container_entity_ids = [prefab_instance.container_entity.id for prefab_instance in prefab_instances] + + duplicate_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DuplicateEntitiesInInstance', container_entity_ids) + assert duplicate_prefab_result.IsSuccess(), f"Prefab operation 'DuplicateEntitiesInInstance' failed. Error: {duplicate_prefab_result.GetError()}" + + wait_for_propagation() + + duplicate_container_entity_ids = duplicate_prefab_result.GetValue() + common_parent_children_ids_after_duplicate = set([child_id.ToString() for child_id in common_parent.get_children_ids()]) + + assert set([container_entity_id.ToString() for container_entity_id in container_entity_ids]).issubset(common_parent_children_ids_after_duplicate), \ + "Provided prefab instances are *not* the children of their common parent anymore after duplication." + assert common_parent_children_ids_before_duplicate.issubset(common_parent_children_ids_after_duplicate), \ + "Some children of provided entities' common parent before duplication are *not* the children of the common parent anymore after duplication." + assert len(common_parent_children_ids_after_duplicate) == len(common_parent_children_ids_before_duplicate) + len(prefab_instances), \ + "The children count of the given prefab instances' common parent entity is *not* increased to the expected number." + assert EditorEntity(duplicate_container_entity_ids[0]).get_parent_id().ToString() == common_parent.id.ToString(), \ + "Provided prefab instances' parent should be the same as duplicates' parent." + + duplicate_instances = [] + for duplicate_container_entity_id in duplicate_container_entity_ids: + prefab_file_path = prefab.PrefabPublicRequestBus(bus.Broadcast, 'GetOwningInstancePrefabPath', duplicate_container_entity_id) + assert prefab_file_path, "Returned file path should *not* be empty." + + prefab_file_name = Path(prefab_file_path).stem + duplicate_instance_prefab = Prefab.get_prefab(prefab_file_name) + duplicate_instance = PrefabInstance(prefab_file_path, EditorEntity(duplicate_container_entity_id)) + duplicate_instance_prefab.instances.add(duplicate_instance) + duplicate_instances.append(duplicate_instance) + + return duplicate_instances + + @classmethod + def detach_prefab(cls, prefab_instance: PrefabInstance): + """ + Detach target prefab instance. + :param prefab_instances: Instance to be detached. + """ + parent = EditorEntity(prefab_instance.container_entity.get_parent_id()) + parent_children_ids_before_detach = set([child_id.ToString() for child_id in parent.get_children_ids()]) + + assert prefab_instance.has_editor_prefab_component(), f"Container entity should have EditorPrefabComponent before detachment." + + detach_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DetachPrefab', prefab_instance.container_entity.id) + assert detach_prefab_result.IsSuccess(), f"Prefab operation 'DetachPrefab' failed. Error: {detach_prefab_result.GetError()}" + + assert not prefab_instance.has_editor_prefab_component(), f"Container entity should *not* have EditorPrefabComponent after detachment." + + parent_children_ids_after_detach = set([child_id.ToString() for child_id in parent.get_children_ids()]) + + assert prefab_instance.container_entity.id.ToString() in parent_children_ids_after_detach, \ + "Target prefab instance's container entity id should still exists after the detachment and before the propagation." + + assert len(parent_children_ids_after_detach) == len(parent_children_ids_before_detach), \ + "Parent entity should still keep the same amount of children entities." + + wait_for_propagation() + + instance_owner_prefab = Prefab.get_prefab(prefab_instance.prefab_file_name) + instance_owner_prefab.instances.remove(prefab_instance) + prefab_instance = PrefabInstance() + + def instantiate(self, parent_entity: EditorEntity=None, name: str=None, prefab_position: Vector3=Vector3()) -> PrefabInstance: + """ + Instantiate an instance of this prefab. + :param parent_entity: The entity the prefab should be a child of in the transform hierarchy. + :param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name. + :param prefab_position: The position in world space the prefab should be instantiated in. + :return: Instantiated PrefabInstance object owned by this prefab. + """ + parent_entity_id = parent_entity.id if parent_entity is not None else EntityId() + + instantiate_prefab_result = prefab.PrefabPublicRequestBus( + bus.Broadcast, 'InstantiatePrefab', self.file_path, parent_entity_id, prefab_position) + + assert instantiate_prefab_result.IsSuccess(), f"Prefab operation 'InstantiatePrefab' failed. Error: {instantiate_prefab_result.GetError()}" + + container_entity_id = instantiate_prefab_result.GetValue() + container_entity = EditorEntity(container_entity_id) + + if name: + container_entity.set_name(name) + + wait_for_propagation() + + new_prefab_instance = PrefabInstance(self.file_path, EditorEntity(container_entity_id)) + assert not new_prefab_instance in self.instances, "This prefab instance is already existed before this instantiation." + self.instances.add(new_prefab_instance) + + assert new_prefab_instance.is_at_position(prefab_position), "This prefab instance is *not* at expected position." + + return new_prefab_instance diff --git a/AutomatedTesting/Gem/PythonTests/Physics/TestSuite_Utils.py b/AutomatedTesting/Gem/PythonTests/Physics/TestSuite_Utils.py index a3fd5c741f..61c2816f50 100755 --- a/AutomatedTesting/Gem/PythonTests/Physics/TestSuite_Utils.py +++ b/AutomatedTesting/Gem/PythonTests/Physics/TestSuite_Utils.py @@ -9,20 +9,17 @@ import pytest import sys import ly_test_tools.environment.file_system as fs -from .FileManagement import FileManagement as fm +from .utils.FileManagement import FileManagement as fm sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../automatedtesting_shared') -from .base import TestAutomationBase +from base import TestAutomationBase - -@pytest.mark.parametrize("platform", ["win_x64_vs2017"]) -@pytest.mark.parametrize("configuration", ["profile"]) -@pytest.mark.parametrize("spec", ["all"]) +@pytest.mark.parametrize("launcher_platform", ['windows_editor']) @pytest.mark.parametrize("project", ["AutomatedTesting"]) class TestUtils(TestAutomationBase): @fm.file_revert("UtilTest_Physmaterial_Editor_TestLibrary.physmaterial", r"AutomatedTesting\Levels\Physics\Physmaterial_Editor_Test") - def test_physmaterial_editor(self, request, workspace, editor): + def test_physmaterial_editor(self, request, workspace, launcher_platform, editor): """ Tests functionality of physmaterial editing utility :param workspace: Fixture containing platform and project detail @@ -35,11 +32,11 @@ class TestUtils(TestAutomationBase): unexpected_lines = ["Assert"] self._run_test(request, workspace, editor, physmaterial_editor_test_module, expected_lines, unexpected_lines) - def test_UtilTest_Tracer_PicksErrorsAndWarnings(self, request, workspace, editor): + def test_UtilTest_Tracer_PicksErrorsAndWarnings(self, request, workspace, launcher_platform, editor): from .utils import UtilTest_Tracer_PicksErrorsAndWarnings as testcase_module self._run_test(request, workspace, editor, testcase_module, [], []) - def test_FileManagement_FindingFiles(self, workspace): + def test_FileManagement_FindingFiles(self, workspace, launcher_platform): """ Tests the functionality of "searching for files" with FileManagement._find_files() :param workspace: ly_test_tools workspace fixture @@ -110,7 +107,7 @@ class TestUtils(TestAutomationBase): find_me_too_path, found_me["FindMeToo.txt"] ) - def test_FileManagement_FileBackup(self, workspace): + def test_FileManagement_FileBackup(self, workspace, launcher_platform): """ Tests the functionality of the file back up system via the FileManagement class :param workspace: ly_test_tools workspace fixture @@ -167,7 +164,7 @@ class TestUtils(TestAutomationBase): del file_map[target_file_path] fm._save_file_map(file_map) - def test_FileManagement_FileRestoration(self, workspace): + def test_FileManagement_FileRestoration(self, workspace, launcher_platform): """ Tests the restore file system via the FileManagement class :param workspace: ly_test_tools workspace fixture @@ -261,7 +258,7 @@ class TestUtils(TestAutomationBase): ["FindMe.txt", "FindMeToo.txt"], parent_path=r"AutomatedTesting\levels\Utils\Managed_files", search_subdirs=True ) @fm.file_override("default.physxconfiguration", "UtilTest_PhysxConfig_Override.physxconfiguration") - def test_UtilTest_Managed_Files(self, request, workspace, editor): + def test_UtilTest_Managed_Files(self, request, workspace, editor, launcher_platform): from .utils import UtilTest_Managed_Files as test_module expected_lines = [] diff --git a/AutomatedTesting/Gem/PythonTests/prefab/CMakeLists.txt b/AutomatedTesting/Gem/PythonTests/Prefab/CMakeLists.txt similarity index 100% rename from AutomatedTesting/Gem/PythonTests/prefab/CMakeLists.txt rename to AutomatedTesting/Gem/PythonTests/Prefab/CMakeLists.txt diff --git a/AutomatedTesting/Gem/PythonTests/prefab/TestSuite_Main.py b/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py similarity index 54% rename from AutomatedTesting/Gem/PythonTests/prefab/TestSuite_Main.py rename to AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py index 09df962b0b..5337f0669c 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/TestSuite_Main.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py @@ -29,21 +29,29 @@ class TestAutomation(TestAutomationBase): autotest_mode=autotest_mode) def test_PrefabLevel_OpensLevelWithEntities(self, request, workspace, editor, launcher_platform): - from . import PrefabLevel_OpensLevelWithEntities as test_module + from .tests import PrefabLevel_OpensLevelWithEntities as test_module self._run_prefab_test(request, workspace, editor, test_module) - def test_Prefab_BasicWorkflow_CreatePrefab(self, request, workspace, editor, launcher_platform): - from . import Prefab_BasicWorkflow_CreatePrefab as test_module + def test_PrefabBasicWorkflow_CreatePrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabBasicWorkflow_CreatePrefab as test_module self._run_prefab_test(request, workspace, editor, test_module) - def test_Prefab_BasicWorkflow_InstantiatePrefab(self, request, workspace, editor, launcher_platform): - from . import Prefab_BasicWorkflow_InstantiatePrefab as test_module + def test_PrefabBasicWorkflow_InstantiatePrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabBasicWorkflow_InstantiatePrefab as test_module self._run_prefab_test(request, workspace, editor, test_module) - def test_Prefab_BasicWorkflow_CreateAndDeletePrefab(self, request, workspace, editor, launcher_platform): - from . import Prefab_BasicWorkflow_CreateAndDeletePrefab as test_module + def test_PrefabBasicWorkflow_CreateAndDeletePrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabBasicWorkflow_CreateAndDeletePrefab as test_module self._run_prefab_test(request, workspace, editor, test_module) - def test_Prefab_BasicWorkflow_CreateAndReparentPrefab(self, request, workspace, editor, launcher_platform): - from . import Prefab_BasicWorkflow_CreateAndReparentPrefab as test_module + def test_PrefabBasicWorkflow_CreateAndReparentPrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabBasicWorkflow_CreateAndReparentPrefab as test_module self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False) + + def test_PrefabBasicWorkflow_CreateReparentAndDetachPrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabBasicWorkflow_CreateReparentAndDetachPrefab as test_module + self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False) + + def test_PrefabBasicWorkflow_CreateAndDuplicatePrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabBasicWorkflow_CreateAndDuplicatePrefab as test_module + self._run_prefab_test(request, workspace, editor, test_module) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/__init__.py b/AutomatedTesting/Gem/PythonTests/Prefab/__init__.py similarity index 100% rename from AutomatedTesting/Gem/PythonTests/prefab/__init__.py rename to AutomatedTesting/Gem/PythonTests/Prefab/__init__.py diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Test.prefab b/AutomatedTesting/Gem/PythonTests/Prefab/data/Test.prefab similarity index 100% rename from AutomatedTesting/Gem/PythonTests/prefab/Test.prefab rename to AutomatedTesting/Gem/PythonTests/Prefab/data/Test.prefab diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndDeletePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py similarity index 63% rename from AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndDeletePrefab.py rename to AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py index f3fbcfa6ad..bbebd70e04 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndDeletePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py @@ -5,29 +5,28 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -def Prefab_BasicWorkflow_CreateAndDeletePrefab(): +def PrefabBasicWorkflow_CreateAndDeletePrefab(): CAR_PREFAB_FILE_NAME = 'car_prefab' from editor_python_test_tools.editor_entity_utils import EditorEntity - from prefab.Prefab import Prefab + from editor_python_test_tools.prefab_utils import Prefab - import prefab.Prefab_Test_Utils as prefab_test_utils + import PrefabTestUtils as prefab_test_utils prefab_test_utils.open_base_tests_level() - # Creates a new Entity at the root level - # Asserts if creation didn't succeed + # Creates a new entity at the root level car_entity = EditorEntity.create_editor_entity() car_prefab_entities = [car_entity] - # Checks for prefab creation passed or not + # Creates a prefab from the new entity _, car = Prefab.create_prefab( car_prefab_entities, CAR_PREFAB_FILE_NAME) - # Checks for prefab deletion passed or not + # Deletes the prefab instance Prefab.remove_prefabs([car]) if __name__ == "__main__": from editor_python_test_tools.utils import Report - Report.start_test(Prefab_BasicWorkflow_CreateAndDeletePrefab) + Report.start_test(PrefabBasicWorkflow_CreateAndDeletePrefab) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDuplicatePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDuplicatePrefab.py new file mode 100644 index 0000000000..2479ae549e --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDuplicatePrefab.py @@ -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 +""" + +def PrefabBasicWorkflow_CreateAndDuplicatePrefab(): + + CAR_PREFAB_FILE_NAME = 'car_prefab' + + from editor_python_test_tools.editor_entity_utils import EditorEntity + from editor_python_test_tools.prefab_utils import Prefab + + import PrefabTestUtils as prefab_test_utils + + prefab_test_utils.open_base_tests_level() + + # Creates a new entity at the root level + car_entity = EditorEntity.create_editor_entity() + car_prefab_entities = [car_entity] + + # Creates a prefab from the new entity + _, car = Prefab.create_prefab( + car_prefab_entities, CAR_PREFAB_FILE_NAME) + + # Duplicates the prefab instance + Prefab.duplicate_prefabs([car]) + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(PrefabBasicWorkflow_CreateAndDuplicatePrefab) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndReparentPrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py similarity index 67% rename from AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndReparentPrefab.py rename to AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py index e5a9d9930a..1cbc591c29 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndReparentPrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py @@ -5,7 +5,7 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -def Prefab_BasicWorkflow_CreateAndReparentPrefab(): +def PrefabBasicWorkflow_CreateAndReparentPrefab(): CAR_PREFAB_FILE_NAME = 'car_prefab' WHEEL_PREFAB_FILE_NAME = 'wheel_prefab' @@ -16,34 +16,33 @@ def Prefab_BasicWorkflow_CreateAndReparentPrefab(): async def run_test(): from editor_python_test_tools.editor_entity_utils import EditorEntity - from prefab.Prefab import Prefab + from editor_python_test_tools.prefab_utils import Prefab - import prefab.Prefab_Test_Utils as prefab_test_utils + import PrefabTestUtils as prefab_test_utils prefab_test_utils.open_base_tests_level() - # Creates a new Entity at the root level - # Asserts if creation didn't succeed + # Creates a new car entity at the root level car_entity = EditorEntity.create_editor_entity() car_prefab_entities = [car_entity] - # Checks for prefab creation passed or not + # Creates a prefab from the car entity _, car = Prefab.create_prefab( car_prefab_entities, CAR_PREFAB_FILE_NAME) - # Creates another new Entity at the root level + # Creates another new wheel entity at the root level wheel_entity = EditorEntity.create_editor_entity() wheel_prefab_entities = [wheel_entity] - # Checks for wheel prefab creation passed or not + # Creates another prefab from the wheel entity _, wheel = Prefab.create_prefab( wheel_prefab_entities, WHEEL_PREFAB_FILE_NAME) - # Checks for prefab reparenting passed or not + # Reparents the wheel prefab instance to the container entity of the car prefab instance await wheel.ui_reparent_prefab_instance(car.container_entity.id) run_test() if __name__ == "__main__": from editor_python_test_tools.utils import Report - Report.start_test(Prefab_BasicWorkflow_CreateAndReparentPrefab) + Report.start_test(PrefabBasicWorkflow_CreateAndReparentPrefab) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreatePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py similarity index 67% rename from AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreatePrefab.py rename to AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py index d86b03d5ad..cae105a9a9 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreatePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py @@ -5,26 +5,25 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -def Prefab_BasicWorkflow_CreatePrefab(): +def PrefabBasicWorkflow_CreatePrefab(): CAR_PREFAB_FILE_NAME = 'car_prefab' from editor_python_test_tools.editor_entity_utils import EditorEntity from editor_python_test_tools.utils import Report - from prefab.Prefab import Prefab + from editor_python_test_tools.prefab_utils import Prefab - import prefab.Prefab_Test_Utils as prefab_test_utils + import PrefabTestUtils as prefab_test_utils prefab_test_utils.open_base_tests_level() - # Creates a new Entity at the root level - # Asserts if creation didn't succeed + # Creates a new entity at the root level car_entity = EditorEntity.create_editor_entity() car_prefab_entities = [car_entity] - # Checks for prefab creation passed or not + # Creates a prefab from the new entity Prefab.create_prefab(car_prefab_entities, CAR_PREFAB_FILE_NAME) if __name__ == "__main__": from editor_python_test_tools.utils import Report - Report.start_test(Prefab_BasicWorkflow_CreatePrefab) + Report.start_test(PrefabBasicWorkflow_CreatePrefab) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateReparentAndDetachPrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateReparentAndDetachPrefab.py new file mode 100644 index 0000000000..bdf77c4bf3 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateReparentAndDetachPrefab.py @@ -0,0 +1,51 @@ +""" +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 +""" + +def PrefabBasicWorkflow_CreateReparentAndDetachPrefab(): + + CAR_PREFAB_FILE_NAME = 'car_prefab' + WHEEL_PREFAB_FILE_NAME = 'wheel_prefab' + + import editor_python_test_tools.pyside_utils as pyside_utils + + @pyside_utils.wrap_async + async def run_test(): + + from editor_python_test_tools.editor_entity_utils import EditorEntity + from editor_python_test_tools.prefab_utils import Prefab + + import PrefabTestUtils as prefab_test_utils + + prefab_test_utils.open_base_tests_level() + + # Creates a new car entity at the root level + car_entity = EditorEntity.create_editor_entity() + car_prefab_entities = [car_entity] + + # Creates a prefab from the car entity + _, car = Prefab.create_prefab( + car_prefab_entities, CAR_PREFAB_FILE_NAME) + + # Creates another new wheel entity at the root level + wheel_entity = EditorEntity.create_editor_entity() + wheel_prefab_entities = [wheel_entity] + + # Creates another prefab from the wheel entity + _, wheel = Prefab.create_prefab( + wheel_prefab_entities, WHEEL_PREFAB_FILE_NAME) + + # Reparents the wheel prefab instance to the container entity of the car prefab instance + await wheel.ui_reparent_prefab_instance(car.container_entity.id) + + # Detaches the wheel prefab instance + Prefab.detach_prefab(wheel) + + run_test() + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(PrefabBasicWorkflow_CreateReparentAndDetachPrefab) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_InstantiatePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py similarity index 70% rename from AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_InstantiatePrefab.py rename to AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py index 46be669697..a701802cd4 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_InstantiatePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py @@ -5,23 +5,22 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -def Prefab_BasicWorkflow_InstantiatePrefab(): +def PrefabBasicWorkflow_InstantiatePrefab(): from azlmbr.math import Vector3 - EXISTING_TEST_PREFAB_FILE_NAME = "Test" + EXISTING_TEST_PREFAB_FILE_NAME = "Gem/PythonTests/Prefab/data/Test.prefab" INSTANTIATED_TEST_PREFAB_POSITION = Vector3(10.00, 20.0, 30.0) EXPECTED_TEST_PREFAB_CHILDREN_COUNT = 1 - from prefab.Prefab import Prefab + from editor_python_test_tools.prefab_utils import Prefab - import prefab.Prefab_Test_Utils as prefab_test_utils + import PrefabTestUtils as prefab_test_utils prefab_test_utils.open_base_tests_level() - # Checks for prefab instantiation passed or not + # Instantiates a new car prefab instance test_prefab = Prefab.get_prefab(EXISTING_TEST_PREFAB_FILE_NAME) - test_instance = test_prefab.instantiate( prefab_position=INSTANTIATED_TEST_PREFAB_POSITION) @@ -31,4 +30,4 @@ def Prefab_BasicWorkflow_InstantiatePrefab(): if __name__ == "__main__": from editor_python_test_tools.utils import Report - Report.start_test(Prefab_BasicWorkflow_InstantiatePrefab) + Report.start_test(PrefabBasicWorkflow_InstantiatePrefab) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/PrefabLevel_OpensLevelWithEntities.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabLevel_OpensLevelWithEntities.py similarity index 87% rename from AutomatedTesting/Gem/PythonTests/prefab/PrefabLevel_OpensLevelWithEntities.py rename to AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabLevel_OpensLevelWithEntities.py index bc95596845..0eb7e86a9e 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/PrefabLevel_OpensLevelWithEntities.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabLevel_OpensLevelWithEntities.py @@ -7,9 +7,9 @@ SPDX-License-Identifier: Apache-2.0 OR MIT # fmt:off class Tests(): - find_empty_entity = ("Entity: 'EmptyEntity' found", "Entity: 'EmptyEntity' *not* found in level") - empty_entity_pos = ("'EmptyEntity' position is at the expected position", "'EmptyEntity' position is *not* at the expected position") - find_pxentity = ("Entity: 'EntityWithPxCollider' found", "Entity: 'EntityWithPxCollider' *not* found in level") + find_empty_entity = ("Entity: 'EmptyEntity' found", "Entity: 'EmptyEntity' *not* found in level") + empty_entity_pos = ("'EmptyEntity' position is at the expected position", "'EmptyEntity' position is *not* at the expected position") + find_pxentity = ("Entity: 'EntityWithPxCollider' found", "Entity: 'EntityWithPxCollider' *not* found in level") pxentity_component = ("Entity: 'EntityWithPxCollider' has a Physx Collider", "Entity: 'EntityWithPxCollider' does *not* have a Physx Collider") # fmt:on diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py new file mode 100644 index 0000000000..f865daf41a --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py @@ -0,0 +1,38 @@ +""" +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 + +from azlmbr.entity import EntityId +from azlmbr.math import Vector3 +from editor_python_test_tools.editor_entity_utils import EditorEntity +from editor_python_test_tools.utils import Report +from editor_python_test_tools.utils import TestHelper as helper + +import azlmbr.bus as bus +import azlmbr.components as components +import azlmbr.entity as entity +import azlmbr.legacy.general as general + +def check_entity_children_count(entity_id, expected_children_count): + entity_children_count_matched_result = ( + "Entity with a unique name found", + "Entity with a unique name *not* found") + + entity = EditorEntity(entity_id) + children_entity_ids = entity.get_children_ids() + entity_children_count_matched = len(children_entity_ids) == expected_children_count + Report.result(entity_children_count_matched_result, entity_children_count_matched) + + if not entity_children_count_matched: + Report.info(f"Entity '{entity_id.ToString()}' actual children count: {len(children_entity_ids)}. Expected children count: {expected_children_count}") + + return entity_children_count_matched + +def open_base_tests_level(): + helper.init_idle() + helper.open_level("Prefab", "Base") diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/Motion/Jack_Idle_Aim_ZUp.dbgsg b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/Motion/Jack_Idle_Aim_ZUp.dbgsg new file mode 100644 index 0000000000..8b0132a448 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/Motion/Jack_Idle_Aim_ZUp.dbgsg @@ -0,0 +1,1515 @@ +ProductName: Jack_Idle_Aim_ZUp.dbgsg +debugSceneGraphVersion: 1 +Jack_Idle_Aim_ZUp +Node Name: RootNode +Node Path: RootNode +Node Type: RootBoneData + WorldTransform: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: jack_root +Node Path: RootNode.jack_root +Node Type: BoneData + WorldTransform: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: animation +Node Path: RootNode.jack_root.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 8622607111849624366 + TimeStepBetweenFrames: 0.033333 + +Node Name: Bip01__pelvis +Node Path: RootNode.jack_root.Bip01__pelvis +Node Type: BoneData + WorldTransform: + BasisX: < 0.987221, 0.158526, -0.016273> + BasisY: <-0.158404, 0.987337, 0.008581> + BasisZ: < 0.017427, -0.005894, 0.999831> + Transl: < 0.002174, 0.008085, 1.014749> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 14058466958533286666 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.transform +Node Type: TransformData + Matrix: + BasisX: < 0.987221, 0.158526, -0.016273> + BasisY: <-0.158404, 0.987337, 0.008581> + BasisZ: < 0.017427, -0.005894, 0.999831> + Transl: < 0.002174, 0.008085, 1.014749> + +Node Name: l_upLeg +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg +Node Type: BoneData + WorldTransform: + BasisX: < 0.922503, 0.382949, -0.048354> + BasisY: <-0.384989, 0.921878, -0.043862> + BasisZ: < 0.027780, 0.059079, 0.997867> + Transl: <-0.099524, 0.003596, 0.983744> + +Node Name: r_upLeg +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg +Node Type: BoneData + WorldTransform: + BasisX: < 0.991893, -0.112768, 0.058580> + BasisY: < 0.104333, 0.985852, 0.131189> + BasisZ: <-0.072545, -0.124013, 0.989625> + Transl: < 0.099120, 0.035493, 0.980470> + +Node Name: spine1 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1 +Node Type: BoneData + WorldTransform: + BasisX: <-0.988827, -0.149057, -0.001499> + BasisY: < 0.003444, -0.032898, 0.999453> + BasisZ: <-0.149025, 0.988281, 0.033044> + Transl: <-0.003015, 0.024799, 1.081740> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 15600598202424573783 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg.transform +Node Type: TransformData + Matrix: + BasisX: < 0.972208, 0.231557, -0.034526> + BasisY: <-0.233213, 0.970812, -0.055997> + BasisZ: < 0.020552, 0.062493, 0.997834> + Transl: <-0.100606, 0.011412, -0.032745> + +Node Name: l_upLegRoll +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg.l_upLegRoll +Node Type: BoneData + WorldTransform: + BasisX: < 0.922503, 0.382949, -0.048354> + BasisY: <-0.384989, 0.921878, -0.043862> + BasisZ: < 0.027780, 0.059079, 0.997867> + Transl: <-0.105719, -0.009578, 0.761220> + +Node Name: l_loLeg +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg.l_loLeg +Node Type: BoneData + WorldTransform: + BasisX: < 0.922503, 0.382949, -0.048355> + BasisY: <-0.385744, 0.910182, -0.150897> + BasisZ: <-0.013775, 0.157856, 0.987366> + Transl: <-0.111914, -0.022752, 0.538696> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 3881375132663304109 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg.transform +Node Type: TransformData + Matrix: + BasisX: < 0.960387, -0.267956, 0.076521> + BasisY: < 0.257148, 0.957967, 0.127175> + BasisZ: <-0.107382, -0.102460, 0.988924> + Transl: < 0.100610, 0.011411, -0.032745> + +Node Name: r_upLegRoll +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg.r_upLegRoll +Node Type: BoneData + WorldTransform: + BasisX: < 0.991893, -0.112768, 0.058580> + BasisY: < 0.104333, 0.985852, 0.131189> + BasisZ: <-0.072545, -0.124013, 0.989625> + Transl: < 0.115298, 0.063148, 0.759784> + +Node Name: r_loLeg +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg.r_loLeg +Node Type: BoneData + WorldTransform: + BasisX: < 0.991893, -0.112768, 0.058580> + BasisY: < 0.118181, 0.988037, -0.099079> + BasisZ: <-0.046706, 0.105199, 0.993354> + Transl: < 0.131475, 0.090804, 0.539097> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 14286132743595604583 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.transform +Node Type: TransformData + Matrix: + BasisX: <-0.999796, 0.009451, -0.017853> + BasisY: <-0.018079, -0.024451, 0.999538> + BasisZ: < 0.009010, 0.999656, 0.024617> + Transl: <-0.003564, 0.017900, 0.066792> + +Node Name: spine2 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2 +Node Type: BoneData + WorldTransform: + BasisX: <-0.984677, -0.174366, -0.002764> + BasisY: <-0.004079, 0.007184, 0.999966> + BasisZ: <-0.174340, 0.984655, -0.007786> + Transl: <-0.002499, 0.019865, 1.231658> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg.l_upLegRoll.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 11375302191090723133 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg.l_upLegRoll.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000000, -0.223000> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg.l_loLeg.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 10115359583353800560 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg.l_loLeg.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, -0.000000> + BasisY: <-0.000000, 0.994203, -0.107519> + BasisZ: < 0.000000, 0.107519, 0.994203> + Transl: < 0.000000, 0.000001, -0.446000> + +Node Name: l_ankle +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg.l_loLeg.l_ankle +Node Type: BoneData + WorldTransform: + BasisX: < 0.963606, 0.267327, 0.000000> + BasisY: <-0.267327, 0.963606, 0.000000> + BasisZ: <-0.000000, -0.000000, 1.000000> + Transl: <-0.105700, -0.093967, 0.093254> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg.r_upLegRoll.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 3742546113399553362 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg.r_upLegRoll.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000000, -0.223000> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg.r_loLeg.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 1660638182832107032 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg.r_loLeg.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.973390, -0.229154> + BasisZ: <-0.000000, 0.229154, 0.973390> + Transl: < 0.000000, 0.000001, -0.446000> + +Node Name: r_ankle +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg.r_loLeg.r_ankle +Node Type: BoneData + WorldTransform: + BasisX: < 1.000000, -0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: <-0.000000, -0.000000, 1.000000> + Transl: < 0.152547, 0.043345, 0.090955> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 10080837380932203667 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.transform +Node Type: TransformData + Matrix: + BasisX: < 0.999670, -0.000417, -0.025672> + BasisY: < 0.001464, 0.999168, 0.040751> + BasisZ: < 0.025634, -0.040775, 0.998839> + Transl: <-0.000000, 0.150000, -0.000000> + +Node Name: spine3 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3 +Node Type: BoneData + WorldTransform: + BasisX: <-0.979537, -0.201214, -0.004467> + BasisY: < 0.008889, -0.065425, 0.997818> + BasisZ: <-0.201067, 0.977360, 0.065875> + Transl: <-0.003111, 0.020942, 1.381653> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg.l_loLeg.l_ankle.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 4299361259356538796 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg.l_loLeg.l_ankle.transform +Node Type: TransformData + Matrix: + BasisX: < 0.991302, -0.128389, 0.028926> + BasisY: < 0.122402, 0.980177, 0.155793> + BasisZ: <-0.048355, -0.150898, 0.987366> + Transl: <-0.000000, 0.000001, -0.451141> + +Node Name: l_ball +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg.l_loLeg.l_ankle.l_ball +Node Type: BoneData + WorldTransform: + BasisX: < 0.963606, 0.267327, 0.000000> + BasisY: <-0.267327, 0.963606, 0.000000> + BasisZ: <-0.000000, -0.000000, 1.000000> + Transl: <-0.145845, 0.050741, 0.026066> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg.r_loLeg.r_ankle.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 11146861401725178254 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg.r_loLeg.r_ankle.transform +Node Type: TransformData + Matrix: + BasisX: < 0.991893, 0.118181, -0.046706> + BasisY: <-0.112767, 0.988037, 0.105199> + BasisZ: < 0.058580, -0.099080, 0.993354> + Transl: <-0.000000, 0.000001, -0.451141> + +Node Name: r_ball +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg.r_loLeg.r_ankle.r_ball +Node Type: BoneData + WorldTransform: + BasisX: < 1.000000, -0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: <-0.000000, -0.000000, 1.000000> + Transl: < 0.152547, 0.193519, 0.023766> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 12335240115610862178 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.transform +Node Type: TransformData + Matrix: + BasisX: < 0.999625, -0.001917, -0.027319> + BasisY: <-0.000102, 0.997277, -0.073739> + BasisZ: < 0.027386, 0.073714, 0.996903> + Transl: <-0.000000, 0.150000, -0.000000> + +Node Name: neck +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.neck +Node Type: BoneData + WorldTransform: + BasisX: <-0.994865, -0.098844, 0.021771> + BasisY: < 0.011332, 0.104974, 0.994410> + BasisZ: <-0.100577, 0.989550, -0.103314> + Transl: <-0.001422, 0.008511, 1.571239> + +Node Name: l_shldr +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr +Node Type: BoneData + WorldTransform: + BasisX: <-0.253998, 0.966419, -0.038967> + BasisY: <-0.966745, -0.254913, -0.020579> + BasisZ: <-0.029821, 0.032444, 0.999029> + Transl: <-0.080266, -0.013371, 1.496066> + +Node Name: r_shldr +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr +Node Type: BoneData + WorldTransform: + BasisX: < 0.391300, -0.910895, 0.130973> + BasisY: <-0.901965, -0.407856, -0.141823> + BasisZ: < 0.182604, -0.062637, -0.981189> + Transl: < 0.080319, 0.019616, 1.496799> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg.l_loLeg.l_ankle.l_ball.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 14823184327075459882 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.l_upLeg.l_loLeg.l_ankle.l_ball.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: <-0.000000, 0.150173, -0.067188> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg.r_loLeg.r_ankle.r_ball.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 7047410889883943662 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.r_upLeg.r_loLeg.r_ankle.r_ball.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: <-0.000000, 0.150173, -0.067188> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.neck.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 17654777499449634575 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.neck.transform +Node Type: TransformData + Matrix: + BasisX: < 0.994299, 0.019348, 0.104862> + BasisY: <-0.036664, 0.985473, 0.165825> + BasisZ: <-0.100131, -0.168724, 0.980564> + Transl: <-0.000000, 0.190000, -0.000000> + +Node Name: head +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.neck.head +Node Type: BoneData + WorldTransform: + BasisX: <-0.999482, -0.031040, -0.008521> + BasisY: <-0.007897, -0.020177, 0.999765> + BasisZ: <-0.031204, 0.999314, 0.019921> + Transl: <-0.002055, 0.035117, 1.663090> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 8014068480245264692 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.transform +Node Type: TransformData + Matrix: + BasisX: < 0.054517, -0.104367, 0.993043> + BasisY: < 0.998347, -0.012450, -0.056117> + BasisZ: < 0.018220, 0.994461, 0.103516> + Transl: < 0.081970, 0.115722, -0.010486> + +Node Name: l_upArm +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm +Node Type: BoneData + WorldTransform: + BasisX: <-0.135064, 0.978453, -0.156165> + BasisY: < 0.963903, 0.093252, -0.249389> + BasisZ: <-0.229453, -0.184212, -0.955729> + Transl: <-0.188150, -0.055591, 1.451722> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 3540966775393318185 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.transform +Node Type: TransformData + Matrix: + BasisX: <-0.200593, 0.193760, -0.960322> + BasisY: < 0.966208, -0.122847, -0.226609> + BasisZ: <-0.161881, -0.973327, -0.162570> + Transl: <-0.081969, 0.115722, -0.010486> + +Node Name: r_upArm +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm +Node Type: BoneData + WorldTransform: + BasisX: <-0.053400, -0.300900, -0.952159> + BasisY: <-0.998561, 0.020802, 0.049429> + BasisZ: < 0.004934, 0.953429, -0.301578> + Transl: < 0.197240, 0.053625, 1.473123> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.neck.head.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 6631764228102045526 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.neck.head.transform +Node Type: TransformData + Matrix: + BasisX: < 0.997232, -0.023058, 0.070690> + BasisY: < 0.031617, 0.991969, -0.122462> + BasisZ: <-0.067299, 0.124358, 0.989953> + Transl: <-0.000000, 0.094124, 0.016902> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 6458355176395918122 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.transform +Node Type: TransformData + Matrix: + BasisX: < 0.985987, -0.115634, -0.120241> + BasisY: <-0.144991, -0.950487, -0.274866> + BasisZ: <-0.082504, 0.288449, -0.953934> + Transl: <-0.011672, 0.115971, -0.042453> + +Node Name: l_upArmRoll +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_upArmRoll +Node Type: BoneData + WorldTransform: + BasisX: <-0.135064, 0.978453, -0.156165> + BasisY: < 0.963903, 0.093252, -0.249389> + BasisZ: <-0.229453, -0.184212, -0.955729> + Transl: <-0.217290, -0.078986, 1.330344> + +Node Name: l_loArm +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm +Node Type: BoneData + WorldTransform: + BasisX: < 0.224836, 0.963610, 0.144585> + BasisY: < 0.971745, -0.210802, -0.106181> + BasisZ: <-0.071838, 0.164373, -0.983779> + Transl: <-0.246431, -0.102381, 1.208967> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 11191005262967074716 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.transform +Node Type: TransformData + Matrix: + BasisX: < 0.128486, 0.305927, 0.943345> + BasisY: <-0.403212, 0.885172, -0.232143> + BasisZ: <-0.906042, -0.350541, 0.237086> + Transl: < 0.011672, -0.115971, 0.042450> + +Node Name: r_upArmRoll +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_upArmRoll +Node Type: BoneData + WorldTransform: + BasisX: <-0.053400, -0.300900, -0.952159> + BasisY: <-0.998561, 0.020802, 0.049429> + BasisZ: < 0.004934, 0.953429, -0.301578> + Transl: < 0.197861, 0.174716, 1.434821> + +Node Name: r_loArm +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm +Node Type: BoneData + WorldTransform: + BasisX: <-0.043387, 0.259999, -0.964634> + BasisY: <-0.998451, 0.022380, 0.050941> + BasisZ: < 0.034833, 0.965349, 0.258625> + Transl: < 0.198492, 0.295797, 1.396522> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_upArmRoll.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 15529789169672670472 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_upArmRoll.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000000, 0.127000> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 1758140872588918890 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.transform +Node Type: TransformData + Matrix: + BasisX: < 0.889900, 0.270521, -0.367282> + BasisY: <-0.320926, 0.943491, -0.082657> + BasisZ: < 0.324166, 0.191427, 0.926430> + Transl: < 0.000000, 0.000000, 0.254000> + +Node Name: l_loArmRoll +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_loArmRoll +Node Type: BoneData + WorldTransform: + BasisX: < 0.224836, 0.963610, 0.144585> + BasisY: < 0.971745, -0.210802, -0.106181> + BasisZ: <-0.071838, 0.164373, -0.983779> + Transl: <-0.256363, -0.079655, 1.072955> + +Node Name: l_hand +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand +Node Type: BoneData + WorldTransform: + BasisX: < 0.514369, 0.855813, 0.054857> + BasisY: < 0.853026, -0.517172, 0.069855> + BasisZ: < 0.088153, 0.010863, -0.996047> + Transl: <-0.266295, -0.056930, 0.936943> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_upArmRoll.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 2211195713271562660 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_upArmRoll.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000005, 0.127006> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 589767551061579979 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.transform +Node Type: TransformData + Matrix: + BasisX: < 0.842568, 0.001053, 0.538588> + BasisY: <-0.001920, 0.999998, 0.001049> + BasisZ: <-0.538586, -0.001918, 0.842568> + Transl: < 0.000000, 0.000001, 0.254001> + +Node Name: r_loArmRoll +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_loArmRoll +Node Type: BoneData + WorldTransform: + BasisX: <-0.043387, 0.259999, -0.964634> + BasisY: <-0.998451, 0.022380, 0.050941> + BasisZ: < 0.034833, 0.965349, 0.258625> + Transl: < 0.203311, 0.429257, 1.432278> + +Node Name: r_hand +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand +Node Type: BoneData + WorldTransform: + BasisX: < 0.047812, -0.119954, -0.991627> + BasisY: <-0.998511, -0.031839, -0.044292> + BasisZ: <-0.026260, 0.992269, -0.121297> + Transl: < 0.208123, 0.562725, 1.468034> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_loArmRoll.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 42494702923975860 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_loArmRoll.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, -0.000000, 0.138254> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 1504211847179885169 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.transform +Node Type: TransformData + Matrix: + BasisX: < 0.948250, 0.313603, 0.049754> + BasisY: <-0.296461, 0.930527, -0.215011> + BasisZ: <-0.113726, 0.189134, 0.975343> + Transl: < 0.000000, 0.000000, 0.276509> + +Node Name: l_thumb1 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_thumb1 +Node Type: BoneData + WorldTransform: + BasisX: < 0.966532, -0.061678, 0.249022> + BasisY: < 0.256526, 0.220065, -0.941151> + BasisZ: < 0.003247, 0.973533, 0.228522> + Transl: <-0.239031, -0.017519, 0.902424> + +Node Name: l_index1 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_index1 +Node Type: BoneData + WorldTransform: + BasisX: < 0.361563, 0.929834, 0.068408> + BasisY: < 0.167767, 0.007290, -0.985800> + BasisZ: <-0.917129, 0.367906, -0.153359> + Transl: <-0.255363, -0.027723, 0.841391> + +Node Name: l_mid1 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_mid1 +Node Type: BoneData + WorldTransform: + BasisX: < 0.329257, 0.944038, -0.019538> + BasisY: < 0.134573, -0.067396, -0.988609> + BasisZ: <-0.934601, 0.322877, -0.149233> + Transl: <-0.261361, -0.046572, 0.840283> + +Node Name: l_metacarpal +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal +Node Type: BoneData + WorldTransform: + BasisX: < 0.521514, 0.839841, -0.150633> + BasisY: <-0.019236, -0.164924, -0.986118> + BasisZ: <-0.853026, 0.517172, -0.069855> + Transl: <-0.269306, -0.070683, 0.893712> + +Node Name: l_handProp +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_handProp +Node Type: BoneData + WorldTransform: + BasisX: < 0.514369, 0.855813, 0.054857> + BasisY: < 0.088153, 0.010863, -0.996047> + BasisZ: <-0.853026, 0.517172, -0.069855> + Transl: <-0.247306, -0.062325, 0.878373> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_loArmRoll.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 10681596377770080497 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_loArmRoll.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: <-0.000001, -0.000004, 0.138251> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 3698507119532421339 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.transform +Node Type: TransformData + Matrix: + BasisX: < 0.923295, -0.100937, -0.370591> + BasisY: < 0.077771, 0.993995, -0.076972> + BasisZ: < 0.376136, 0.042247, 0.925601> + Transl: < 0.000000, 0.000000, 0.276509> + +Node Name: r_thumb1 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_thumb1 +Node Type: BoneData + WorldTransform: + BasisX: < 0.005202, 0.326409, 0.945214> + BasisY: < 0.985042, -0.164476, 0.051377> + BasisZ: < 0.172235, 0.930809, -0.322382> + Transl: < 0.204493, 0.605135, 1.508976> + +Node Name: r_index1 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_index1 +Node Type: BoneData + WorldTransform: + BasisX: <-0.080293, -0.430850, -0.898844> + BasisY: < 0.599061, -0.741585, 0.301956> + BasisZ: <-0.796667, -0.514218, 0.317650> + Transl: < 0.216812, 0.661876, 1.482050> + +Node Name: r_mid1 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_mid1 +Node Type: BoneData + WorldTransform: + BasisX: <-0.026042, -0.464356, -0.885265> + BasisY: < 0.970667, -0.223480, 0.088669> + BasisZ: <-0.239013, -0.856989, 0.456555> + Transl: < 0.213176, 0.659786, 1.462687> + +Node Name: r_metacarpal +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal +Node Type: BoneData + WorldTransform: + BasisX: <-0.193971, -0.324462, -0.925797> + BasisY: < 0.264669, -0.926035, 0.269092> + BasisZ: <-0.944630, -0.192833, 0.265499> + Transl: < 0.206234, 0.603106, 1.447237> + +Node Name: r_handProp +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_handProp +Node Type: BoneData + WorldTransform: + BasisX: < 0.055796, -0.000585, -0.998442> + BasisY: <-0.998185, -0.022714, -0.055768> + BasisZ: <-0.022646, 0.999742, -0.001852> + Transl: < 0.180957, 0.701814, 1.494520> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_thumb1.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 14343621999852722213 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_thumb1.transform +Node Type: TransformData + Matrix: + BasisX: < 0.458030, 0.873770, -0.163505> + BasisY: < 0.268655, 0.039268, 0.962436> + BasisZ: < 0.847368, -0.484751, -0.216757> + Transl: < 0.045859, 0.000463, 0.037215> + +Node Name: l_thumb2 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_thumb1.l_thumb2 +Node Type: BoneData + WorldTransform: + BasisX: < 0.966532, -0.061678, 0.249022> + BasisY: < 0.237349, -0.153444, -0.959229> + BasisZ: < 0.097374, 0.986230, -0.133669> + Transl: <-0.228837, -0.008774, 0.865024> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_index1.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 11739678721039917067 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_index1.transform +Node Type: TransformData + Matrix: + BasisX: < 0.985494, -0.167682, -0.026164> + BasisY: < 0.038455, 0.070476, 0.996772> + BasisZ: <-0.165297, -0.983319, 0.075902> + Transl: < 0.025377, -0.012455, 0.096456> + +Node Name: l_index2 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_index1.l_index2 +Node Type: BoneData + WorldTransform: + BasisX: < 0.361563, 0.929834, 0.068408> + BasisY: < 0.490118, -0.127139, -0.862334> + BasisZ: <-0.793130, 0.345316, -0.501698> + Transl: <-0.247117, -0.027364, 0.792942> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_mid1.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 13442540339145227410 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_mid1.transform +Node Type: TransformData + Matrix: + BasisX: < 0.976208, -0.208730, 0.058741> + BasisY: <-0.042691, 0.080590, 0.995833> + BasisZ: <-0.212594, -0.974647, 0.069762> + Transl: < 0.006100, -0.007900, 0.096826> + +Node Name: l_mid2 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_mid1.l_mid2 +Node Type: BoneData + WorldTransform: + BasisX: < 0.329257, 0.944038, -0.019538> + BasisY: < 0.465563, -0.180309, -0.866452> + BasisZ: <-0.821487, 0.276189, -0.498877> + Transl: <-0.255124, -0.049696, 0.794467> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 974575578739462707 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.transform +Node Type: TransformData + Matrix: + BasisX: < 0.978734, -0.000000, 0.205134> + BasisY: <-0.205134, -0.000000, 0.978734> + BasisZ: < 0.000000, -1.000000, -0.000000> + Transl: <-0.015690, 0.001524, 0.042645> + +Node Name: l_ring1 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_ring1 +Node Type: BoneData + WorldTransform: + BasisX: < 0.355971, 0.932976, -0.053286> + BasisY: < 0.135966, -0.108122, -0.984796> + BasisZ: <-0.924553, 0.343314, -0.165341> + Transl: <-0.267971, -0.066789, 0.843287> + +Node Name: l_pinky1 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_pinky1 +Node Type: BoneData + WorldTransform: + BasisX: < 0.316973, 0.929755, -0.187305> + BasisY: < 0.269800, -0.277722, -0.921997> + BasisZ: <-0.909250, 0.241713, -0.338878> + Transl: <-0.274493, -0.085610, 0.850043> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_handProp.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 12174887080937692924 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_handProp.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000000, 1.000000> + BasisZ: < 0.000000, -1.000000, -0.000000> + Transl: < 0.001937, 0.014896, 0.059955> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_thumb1.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 13049257571504175902 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_thumb1.transform +Node Type: TransformData + Matrix: + BasisX: <-0.976206, -0.057453, 0.209097> + BasisY: < 0.015879, -0.980614, -0.195303> + BasisZ: < 0.216264, -0.187336, 0.958194> + Transl: <-0.045860, 0.000461, 0.037212> + +Node Name: r_thumb2 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_thumb1.r_thumb2 +Node Type: BoneData + WorldTransform: + BasisX: < 0.005202, 0.326409, 0.945214> + BasisY: < 0.528044, -0.803596, 0.274598> + BasisZ: < 0.849202, 0.497686, -0.176538> + Transl: < 0.165349, 0.611670, 1.506935> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_index1.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 5016935426297207355 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_index1.transform + +Node Type: TransformData + Matrix: + BasisX: < 0.939162, 0.133704, -0.316383> + BasisY: <-0.181830, -0.587932, -0.788209> + BasisZ: <-0.291398, 0.797784, -0.527852> + Transl: <-0.025377, -0.012454, 0.096457> + +Node Name: r_index2 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_index1.r_index2 +Node Type: BoneData + WorldTransform: + BasisX: <-0.102387, -0.418082, -0.902621> + BasisY: < 0.928150, 0.286271, -0.237880> + BasisZ: < 0.357847, -0.862123, 0.358732> + Transl: < 0.187367, 0.698324, 1.467209> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_mid1.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 18436012888435130283 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_mid1.transform +Node Type: TransformData + Matrix: + BasisX: < 0.932310, 0.079999, -0.352702> + BasisY: <-0.014710, -0.966034, -0.257996> + BasisZ: <-0.361362, 0.245721, -0.899466> + Transl: <-0.006099, -0.007899, 0.096827> + +Node Name: r_mid2 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_mid1.r_mid2 +Node Type: BoneData + WorldTransform: + BasisX: <-0.026042, -0.464356, -0.885265> + BasisY: < 0.573273, 0.718543, -0.393768> + BasisZ: < 0.818951, -0.517753, 0.247490> + Transl: < 0.168188, 0.670142, 1.458578> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 5132817095621228682 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.transform +Node Type: TransformData + Matrix: + BasisX: < 0.947692, 0.245019, -0.204563> + BasisY: <-0.143103, -0.246709, -0.958465> + BasisZ: <-0.285310, 0.937604, -0.198741> + Transl: < 0.015689, 0.001521, 0.042641> + +Node Name: r_ring1 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_ring1 +Node Type: BoneData + WorldTransform: + BasisX: <-0.065609, -0.462609, -0.884132> + BasisY: < 0.993795, -0.110043, -0.016168> + BasisZ: <-0.089813, -0.879707, 0.466958> + Transl: < 0.199642, 0.653138, 1.443571> + +Node Name: r_pinky1 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_pinky1 +Node Type: BoneData + WorldTransform: + BasisX: <-0.110035, -0.467205, -0.877275> + BasisY: < 0.990191, 0.024933, -0.137476> + BasisZ: < 0.086103, -0.883797, 0.459878> + Transl: < 0.192242, 0.642502, 1.427003> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_handProp.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 13043566764728405902 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_handProp.transform +Node Type: TransformData + Matrix: + BasisX: < 0.992821, -0.011471, 0.119062> + BasisY: < 0.010301, 0.999892, 0.010438> + BasisZ: <-0.119169, -0.009136, 0.992832> + Transl: <-0.044247, 0.021524, 0.135515> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_thumb1.l_thumb2.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 8680475892235074248 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_thumb1.l_thumb2.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.929898, -0.367817> + BasisZ: < 0.000000, 0.367817, 0.929898> + Transl: <-0.000000, 0.039739, 0.000000> + +Node Name: l_thumb3 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_thumb1.l_thumb2.l_thumb3 +Node Type: BoneData + WorldTransform: + BasisX: < 0.966532, -0.061678, 0.249022> + BasisY: < 0.237349, -0.153444, -0.959229> + BasisZ: < 0.097374, 0.986230, -0.133669> + Transl: <-0.221823, -0.013308, 0.836676> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_index1.l_index2.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 9992946931765780388 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_index1.l_index2.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.931387, -0.364030> + BasisZ: < 0.000000, 0.364030, 0.931387> + Transl: <-0.000000, 0.049147, 0.000000> + +Node Name: l_index3 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_index1.l_index2.l_index3 +Node Type: BoneData + WorldTransform: + BasisX: < 0.361563, 0.929834, 0.068408> + BasisY: < 0.745213, -0.244121, -0.620534> + BasisZ: <-0.560294, 0.275341, -0.781190> + Transl: <-0.231135, -0.031510, 0.764821> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_mid1.l_mid2.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 5852602857894555578 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_mid1.l_mid2.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.931387, -0.364030> + BasisZ: < 0.000000, 0.364030, 0.931387> + Transl: <-0.000000, 0.046344, 0.000000> + +Node Name: l_mid3 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_mid1.l_mid2.l_mid3 +Node Type: BoneData + WorldTransform: + BasisX: < 0.329257, 0.944038, -0.019538> + BasisY: < 0.732665, -0.268479, -0.625396> + BasisZ: <-0.595644, 0.191601, -0.780062> + Transl: <-0.239947, -0.055573, 0.766221> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_ring1.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 1267179882835617369 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_ring1.transform +Node Type: TransformData + Matrix: + BasisX: < 0.977223, -0.108172, 0.182579> + BasisY: < 0.128445, 0.986342, -0.103107> + BasisZ: <-0.168932, 0.124210, 0.977770> + Transl: < 0.011562, 0.049057, 0.004398> + +Node Name: l_ring2 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_ring1.l_ring2 +Node Type: BoneData + WorldTransform: + BasisX: < 0.355971, 0.932976, -0.053286> + BasisY: < 0.512261, -0.242505, -0.823881> + BasisZ: <-0.781584, 0.265982, -0.564252> + Transl: <-0.262502, -0.071138, 0.803673> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_pinky1.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 6387671957824055658 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_pinky1.transform +Node Type: TransformData + Matrix: + BasisX: < 0.974367, 0.025268, 0.223541> + BasisY: < 0.046345, 0.949812, -0.309370> + BasisZ: <-0.220139, 0.311800, 0.924294> + Transl: <-0.008664, 0.045625, -0.000244> + +Node Name: l_pinky2 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_pinky1.l_pinky2 +Node Type: BoneData + WorldTransform: + BasisX: < 0.316973, 0.929755, -0.187305> + BasisY: < 0.608398, -0.350828, -0.711879> + BasisZ: <-0.727585, 0.111690, -0.676864> + Transl: <-0.265311, -0.095062, 0.818663> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_thumb1.r_thumb2.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 16242899712758786600 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_thumb1.r_thumb2.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.666425, -0.745572> + BasisZ: < 0.000000, 0.745572, 0.666425> + Transl: < 0.000000, -0.039738, -0.000001> + +Node Name: r_thumb3 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_thumb1.r_thumb2.r_thumb3 +Node Type: BoneData + WorldTransform: + BasisX: < 0.005202, 0.326409, 0.945214> + BasisY: < 0.067094, -0.943211, 0.325348> + BasisZ: < 0.997733, 0.061726, -0.026807> + Transl: < 0.149746, 0.635423, 1.498819> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_index1.r_index2.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 5372640529794878290 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_index1.r_index2.transform +Node Type: TransformData + Matrix: + BasisX: < 0.999667, -0.023844, 0.009837> + BasisY: < 0.015953, 0.271895, -0.962195> + BasisZ: < 0.020268, 0.962031, 0.272185> + Transl: < 0.000001, -0.049150, 0.000002> + +Node Name: r_index3 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_index1.r_index2.r_index3 +Node Type: BoneData + WorldTransform: + BasisX: <-0.102387, -0.418082, -0.902621> + BasisY: < 0.098677, 0.898645, -0.427434> + BasisZ: < 0.989838, -0.132831, -0.050755> + Transl: < 0.157099, 0.688988, 1.474966> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_mid1.r_mid2.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 12073124542497485563 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_mid1.r_mid2.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.360962, -0.932581> + BasisZ: < 0.000000, 0.932581, 0.360962> + Transl: < 0.000001, -0.046347, 0.000002> + +Node Name: r_mid3 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_mid1.r_mid2.r_mid3 +Node Type: BoneData + WorldTransform: + BasisX: <-0.026042, -0.464356, -0.885265> + BasisY: <-0.563987, 0.737995, -0.370516> + BasisZ: < 0.825373, 0.489629, -0.281110> + Transl: < 0.149501, 0.646717, 1.471415> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_ring1.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 8154488711982939451 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_ring1.transform +Node Type: TransformData + Matrix: + BasisX: < 0.981351, 0.173114, -0.083554> + BasisY: <-0.142094, 0.360579, -0.921841> + BasisZ: <-0.129456, 0.916523, 0.378454> + Transl: <-0.011561, -0.049063, -0.004394> + +Node Name: r_ring2 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_ring1.r_ring2 +Node Type: BoneData + WorldTransform: + BasisX: <-0.065609, -0.462609, -0.884132> + BasisY: < 0.321723, 0.828917, -0.457593> + BasisZ: < 0.944558, -0.314468, 0.094448> + Transl: < 0.159665, 0.657564, 1.444222> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_pinky1.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 9369437217348288734 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_pinky1.transform +Node Type: TransformData + Matrix: + BasisX: < 0.985112, 0.167457, -0.038881> + BasisY: <-0.072884, 0.201990, -0.976672> + BasisZ: <-0.155697, 0.964965, 0.211187> + Transl: < 0.008664, -0.045630, 0.000249> + +Node Name: r_pinky2 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_pinky1.r_pinky2 +Node Type: BoneData + WorldTransform: + BasisX: <-0.110035, -0.467205, -0.877275> + BasisY: < 0.256704, 0.839331, -0.479194> + BasisZ: < 0.960206, -0.277928, 0.027578> + Transl: < 0.158539, 0.641652, 1.431683> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_thumb1.l_thumb2.l_thumb3.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 4676225233777059829 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_thumb1.l_thumb2.l_thumb3.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: <-0.000000, 0.029553, 0.000000> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_index1.l_index2.l_index3.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 6008238647628856750 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_index1.l_index2.l_index3.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.931387, -0.364030> + BasisZ: < 0.000000, 0.364030, 0.931387> + Transl: <-0.000000, 0.032610, 0.000000> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_mid1.l_mid2.l_mid3.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 15208282716784707655 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_mid1.l_mid2.l_mid3.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.931387, -0.364030> + BasisZ: < 0.000000, 0.364030, 0.931387> + Transl: <-0.000000, 0.032599, 0.000000> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_ring1.l_ring2.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 1878731310508703159 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_ring1.l_ring2.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.907225, -0.420646> + BasisZ: < 0.000000, 0.420646, 0.907225> + Transl: < 0.000000, 0.040226, -0.000000> + +Node Name: l_ring3 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_ring1.l_ring2.l_ring3 +Node Type: BoneData + WorldTransform: + BasisX: < 0.355971, 0.932976, -0.053286> + BasisY: < 0.793506, -0.331890, -0.510095> + BasisZ: <-0.493592, 0.139297, -0.858465> + Transl: <-0.245519, -0.079178, 0.776359> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_pinky1.l_pinky2.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 13581491544008149678 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_pinky1.l_pinky2.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.917929, -0.396745> + BasisZ: < 0.000000, 0.396745, 0.917929> + Transl: < 0.000000, 0.034035, 0.000000> + +Node Name: l_pinky3 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_pinky1.l_pinky2.l_pinky3 +Node Type: BoneData + WorldTransform: + BasisX: < 0.316973, 0.929755, -0.187305> + BasisY: < 0.847132, -0.366347, -0.384912> + BasisZ: <-0.426492, -0.036665, -0.903747> + Transl: <-0.250848, -0.103402, 0.801741> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_thumb1.r_thumb2.r_thumb3.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 10072340935457210394 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_thumb1.r_thumb2.r_thumb3.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.882729, -0.469883> + BasisZ: < 0.000000, 0.469883, 0.882729> + Transl: < 0.000001, -0.029555, 0.000004> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_index1.r_index2.r_index3.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 9406467531174726159 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_index1.r_index2.r_index3.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.450521, -0.892766> + BasisZ: < 0.000000, 0.892766, 0.450521> + Transl: < 0.000000, -0.032611, 0.000000> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_mid1.r_mid2.r_mid3.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 10286873651678877093 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_mid1.r_mid2.r_mid3.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.352861, -0.935676> + BasisZ: < 0.000000, 0.935676, 0.352861> + Transl: < 0.000000, -0.032600, 0.000001> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_ring1.r_ring2.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 440935841590130722 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_ring1.r_ring2.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.235909, -0.971775> + BasisZ: < 0.000000, 0.971775, 0.235909> + Transl: < 0.000000, -0.040227, 0.000001> + +Node Name: r_ring3 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_ring1.r_ring2.r_ring3 +Node Type: BoneData + WorldTransform: + BasisX: <-0.065609, -0.462609, -0.884132> + BasisY: <-0.830725, 0.516186, -0.208441> + BasisZ: < 0.552803, 0.720795, -0.418167> + Transl: < 0.148998, 0.630083, 1.459393> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_pinky1.r_pinky2.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 14713526539244402950 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_pinky1.r_pinky2.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.340991, -0.940067> + BasisZ: < 0.000000, 0.940067, 0.340991> + Transl: < 0.000000, -0.034037, 0.000001> + +Node Name: r_pinky3 +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_pinky1.r_pinky2.r_pinky3 +Node Type: BoneData + WorldTransform: + BasisX: <-0.110035, -0.467205, -0.877275> + BasisY: <-0.875654, 0.463151, -0.136825> + BasisZ: < 0.470236, 0.753134, -0.460072> + Transl: < 0.152437, 0.621701, 1.443073> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_ring1.l_ring2.l_ring3.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 10370667742752731647 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_ring1.l_ring2.l_ring3.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.907225, -0.420646> + BasisZ: < 0.000000, 0.420646, 0.907225> + Transl: <-0.000000, 0.033153, 0.000000> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_pinky1.l_pinky2.l_pinky3.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 8840292647679362561 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.l_shldr.l_upArm.l_loArm.l_hand.l_metacarpal.l_pinky1.l_pinky2.l_pinky3.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.917929, -0.396745> + BasisZ: < 0.000000, 0.396745, 0.917929> + Transl: < 0.000000, 0.023771, 0.000000> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_ring1.r_ring2.r_ring3.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 16473969543279624642 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_ring1.r_ring2.r_ring3.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.255993, -0.966679> + BasisZ: < 0.000000, 0.966679, 0.255993> + Transl: < 0.000000, -0.033153, -0.000000> + +Node Name: animation +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_pinky1.r_pinky2.r_pinky3.animation +Node Type: AnimationData + KeyFrames: Count 195. Hash: 756580873850673075 + TimeStepBetweenFrames: 0.033333 + +Node Name: transform +Node Path: RootNode.jack_root.Bip01__pelvis.spine1.spine2.spine3.r_shldr.r_upArm.r_loArm.r_hand.r_metacarpal.r_pinky1.r_pinky2.r_pinky3.transform +Node Type: TransformData + Matrix: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 0.229519, -0.973304> + BasisZ: < 0.000000, 0.973304, 0.229519> + Transl: < 0.000000, -0.023770, 0.000000> diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/Motion/Jack_Idle_Aim_ZUp.fbx b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/Motion/Jack_Idle_Aim_ZUp.fbx new file mode 100644 index 0000000000..995d2f4ea2 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/Motion/Jack_Idle_Aim_ZUp.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3384e88cd0f47ab8ec81eb75101b02ff9675c76dc070c726a9f3f39f1b2b2df +size 4994448 diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/OneMeshMultipleMaterials/single_mesh_multiple_materials.dbgsg b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/OneMeshMultipleMaterials/single_mesh_multiple_materials.dbgsg index 293a936de0..e1a3d618ad 100644 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/OneMeshMultipleMaterials/single_mesh_multiple_materials.dbgsg +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/OneMeshMultipleMaterials/single_mesh_multiple_materials.dbgsg @@ -1,16 +1,34 @@ ProductName: single_mesh_multiple_materials.dbgsg debugSceneGraphVersion: 1 single_mesh_multiple_materials -Node Name: Torus -Node Path: RootNode.Torus +Node Name: RootNode +Node Path: RootNode +Node Type: RootBoneData + WorldTransform: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: Torus_1 +Node Path: RootNode.Torus.Torus_1 Node Type: MeshData Positions: Count 2304. Hash: 12560656679477605282 Normals: Count 2304. Hash: 14915939258818888021 FaceList: Count 1152. Hash: 3035560221708475304 FaceMaterialIds: Count 1152. Hash: 2033667258170256242 -Node Name: Torus_optimized -Node Path: RootNode.Torus_optimized +Node Name: Torus_2 +Node Path: RootNode.Torus.Torus_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: Torus_1_optimized +Node Path: RootNode.Torus.Torus_1_optimized Node Type: MeshData Positions: Count 2304. Hash: 12560656679477605282 Normals: Count 2304. Hash: 14915939258818888021 @@ -18,7 +36,7 @@ Node Type: MeshData FaceMaterialIds: Count 1152. Hash: 2033667258170256242 Node Name: transform -Node Path: RootNode.Torus.transform +Node Path: RootNode.Torus.Torus_1.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -27,13 +45,13 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: UV0 -Node Path: RootNode.Torus.UV0 +Node Path: RootNode.Torus.Torus_1.UV0 Node Type: MeshVertexUVData UVs: Count 2304. Hash: 6069930558565069665 UVCustomName: UV0 Node Name: OrangeMaterial -Node Path: RootNode.Torus.OrangeMaterial +Node Path: RootNode.Torus.Torus_1.OrangeMaterial Node Type: MaterialData MaterialName: OrangeMaterial UniqueId: 10937477720113828524 @@ -63,7 +81,7 @@ Node Type: MaterialData BaseColorTexture: Node Name: SecondTextureMaterial -Node Path: RootNode.Torus.SecondTextureMaterial +Node Path: RootNode.Torus.Torus_1.SecondTextureMaterial Node Type: MaterialData MaterialName: SecondTextureMaterial UniqueId: 16601413836225607467 @@ -93,7 +111,7 @@ Node Type: MaterialData BaseColorTexture: OneMeshMultipleMaterials/FBXSecondTestTexture.png Node Name: FirstTextureMaterial -Node Path: RootNode.Torus.FirstTextureMaterial +Node Path: RootNode.Torus.Torus_1.FirstTextureMaterial Node Type: MaterialData MaterialName: FirstTextureMaterial UniqueId: 2580020563915538382 @@ -122,40 +140,145 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: OneMeshMultipleMaterials/FBXTestTexture.png -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Torus.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Torus.Torus_1.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 2304. Hash: 17641066831235827929 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Torus.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Torus.Torus_1.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 2304. Hash: 6274616552656695154 - TangentSpace: 1 + GenerationMethod: 1 + +Node Name: transform +Node Path: RootNode.Torus.Torus_2.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: UV0 +Node Path: RootNode.Torus.Torus_2.UV0 +Node Type: MeshVertexUVData + UVs: Count 2304. Hash: 6069930558565069665 + UVCustomName: UV0 + +Node Name: OrangeMaterial +Node Path: RootNode.Torus.Torus_2.OrangeMaterial +Node Type: MaterialData + MaterialName: OrangeMaterial + UniqueId: 10937477720113828524 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.113346, 0.000000> + SpecularColor: < 0.800000, 0.113346, 0.000000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 25.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: + +Node Name: SecondTextureMaterial +Node Path: RootNode.Torus.Torus_2.SecondTextureMaterial +Node Type: MaterialData + MaterialName: SecondTextureMaterial + UniqueId: 16601413836225607467 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.800000, 0.800000, 0.800000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 25.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: OneMeshMultipleMaterials/FBXSecondTestTexture.png + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: OneMeshMultipleMaterials/FBXSecondTestTexture.png + +Node Name: FirstTextureMaterial +Node Path: RootNode.Torus.Torus_2.FirstTextureMaterial +Node Type: MaterialData + MaterialName: FirstTextureMaterial + UniqueId: 2580020563915538382 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.800000, 0.800000, 0.800000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 25.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: OneMeshMultipleMaterials/FBXTestTexture.png + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: OneMeshMultipleMaterials/FBXTestTexture.png Node Name: UV0 -Node Path: RootNode.Torus_optimized.UV0 +Node Path: RootNode.Torus.Torus_1_optimized.UV0 Node Type: MeshVertexUVData UVs: Count 2304. Hash: 6069930558565069665 UVCustomName: UV0 -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Torus_optimized.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Torus.Torus_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 2304. Hash: 17641066831235827929 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Torus_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Torus.Torus_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 2304. Hash: 6274616552656695154 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Torus_optimized.transform +Node Path: RootNode.Torus.Torus_1_optimized.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -164,7 +287,7 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: OrangeMaterial -Node Path: RootNode.Torus_optimized.OrangeMaterial +Node Path: RootNode.Torus.Torus_1_optimized.OrangeMaterial Node Type: MaterialData MaterialName: OrangeMaterial UniqueId: 10937477720113828524 @@ -194,7 +317,7 @@ Node Type: MaterialData BaseColorTexture: Node Name: SecondTextureMaterial -Node Path: RootNode.Torus_optimized.SecondTextureMaterial +Node Path: RootNode.Torus.Torus_1_optimized.SecondTextureMaterial Node Type: MaterialData MaterialName: SecondTextureMaterial UniqueId: 16601413836225607467 @@ -224,7 +347,7 @@ Node Type: MaterialData BaseColorTexture: OneMeshMultipleMaterials/FBXSecondTestTexture.png Node Name: FirstTextureMaterial -Node Path: RootNode.Torus_optimized.FirstTextureMaterial +Node Path: RootNode.Torus.Torus_1_optimized.FirstTextureMaterial Node Type: MaterialData MaterialName: FirstTextureMaterial UniqueId: 2580020563915538382 @@ -252,4 +375,3 @@ Node Type: MaterialData AmbientOcclusionTexture: EmissiveTexture: BaseColorTexture: OneMeshMultipleMaterials/FBXTestTexture.png - diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/OneMeshOneMaterial/onemeshonematerial.dbgsg b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/OneMeshOneMaterial/onemeshonematerial.dbgsg index 658e8c8994..ffe6a8ce71 100644 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/OneMeshOneMaterial/onemeshonematerial.dbgsg +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/OneMeshOneMaterial/onemeshonematerial.dbgsg @@ -1,16 +1,34 @@ ProductName: OneMeshOneMaterial.dbgsg debugSceneGraphVersion: 1 OneMeshOneMaterial -Node Name: Cube -Node Path: RootNode.Cube +Node Name: RootNode +Node Path: RootNode +Node Type: RootBoneData + WorldTransform: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: Cube_1 +Node Path: RootNode.Cube.Cube_1 Node Type: MeshData Positions: Count 24. Hash: 8661923109306356285 Normals: Count 24. Hash: 5807525742165000561 FaceList: Count 12. Hash: 9888799799190757436 FaceMaterialIds: Count 12. Hash: 7110546404675862471 -Node Name: Cube_optimized -Node Path: RootNode.Cube_optimized +Node Name: Cube_2 +Node Path: RootNode.Cube.Cube_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: Cube_1_optimized +Node Path: RootNode.Cube.Cube_1_optimized Node Type: MeshData Positions: Count 24. Hash: 8661923109306356285 Normals: Count 24. Hash: 5807525742165000561 @@ -18,7 +36,7 @@ Node Type: MeshData FaceMaterialIds: Count 12. Hash: 7110546404675862471 Node Name: transform -Node Path: RootNode.Cube.transform +Node Path: RootNode.Cube.Cube_1.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -27,13 +45,13 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.Cube.UVMap +Node Path: RootNode.Cube.Cube_1.UVMap Node Type: MeshVertexUVData UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UVMap Node Name: CubeMaterial -Node Path: RootNode.Cube.CubeMaterial +Node Path: RootNode.Cube.Cube_1.CubeMaterial Node Type: MaterialData MaterialName: CubeMaterial UniqueId: 973942033197978066 @@ -62,40 +80,85 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: OneMeshOneMaterial/FBXTestTexture.png -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cube.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Cube.Cube_1.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 24. Hash: 13438447437797057049 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cube.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cube.Cube_1.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 24. Hash: 11372562338897179017 - TangentSpace: 1 + GenerationMethod: 1 + +Node Name: transform +Node Path: RootNode.Cube.Cube_2.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: UVMap +Node Path: RootNode.Cube.Cube_2.UVMap +Node Type: MeshVertexUVData + UVs: Count 24. Hash: 1622169145591646736 + UVCustomName: UVMap + +Node Name: CubeMaterial +Node Path: RootNode.Cube.Cube_2.CubeMaterial +Node Type: MaterialData + MaterialName: CubeMaterial + UniqueId: 973942033197978066 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.800000, 0.800000, 0.800000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 36.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: OneMeshOneMaterial/FBXTestTexture.png + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: OneMeshOneMaterial/FBXTestTexture.png Node Name: UVMap -Node Path: RootNode.Cube_optimized.UVMap +Node Path: RootNode.Cube.Cube_1_optimized.UVMap Node Type: MeshVertexUVData UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UVMap -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cube_optimized.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Cube.Cube_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 24. Hash: 13438447437797057049 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cube_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cube.Cube_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 24. Hash: 11372562338897179017 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Cube_optimized.transform +Node Path: RootNode.Cube.Cube_1_optimized.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -104,7 +167,7 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: CubeMaterial -Node Path: RootNode.Cube_optimized.CubeMaterial +Node Path: RootNode.Cube.Cube_1_optimized.CubeMaterial Node Type: MaterialData MaterialName: CubeMaterial UniqueId: 973942033197978066 diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/SoftNamingLOD/lodtest.dbgsg b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/SoftNamingLOD/lodtest.dbgsg index 1c39ca312a..90bcb94795 100644 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/SoftNamingLOD/lodtest.dbgsg +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/SoftNamingLOD/lodtest.dbgsg @@ -1,64 +1,109 @@ ProductName: lodtest.dbgsg debugSceneGraphVersion: 1 lodtest -Node Name: lodtest -Node Path: RootNode.lodtest +Node Name: RootNode +Node Path: RootNode +Node Type: RootBoneData + WorldTransform: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: lodtest_1 +Node Path: RootNode.lodtest.lodtest_1 Node Type: MeshData Positions: Count 24. Hash: 8661923109306356285 Normals: Count 24. Hash: 5807525742165000561 FaceList: Count 12. Hash: 9888799799190757436 FaceMaterialIds: Count 12. Hash: 7110546404675862471 -Node Name: lodtest_lod3 -Node Path: RootNode.lodtest_lod3 -Node Type: MeshData - Positions: Count 1984. Hash: 6600975913707260286 - Normals: Count 1984. Hash: 2708036977889843831 - FaceList: Count 960. Hash: 10390417165025722786 - FaceMaterialIds: Count 960. Hash: 12510609185544665964 - -Node Name: lodtest_lod2 -Node Path: RootNode.lodtest_lod2 -Node Type: MeshData - Positions: Count 240. Hash: 219362421205407416 - Normals: Count 240. Hash: 11195242321181199939 - FaceList: Count 80. Hash: 11130917988116538993 - FaceMaterialIds: Count 80. Hash: 4190892684086530065 - -Node Name: lodtest_lod1 -Node Path: RootNode.lodtest_lod1 -Node Type: MeshData - Positions: Count 192. Hash: 1283526254311745349 - Normals: Count 192. Hash: 1873340970602844856 - FaceList: Count 124. Hash: 3728991722746136013 - FaceMaterialIds: Count 124. Hash: 2372486708814455910 +Node Name: lodtest_2 +Node Path: RootNode.lodtest.lodtest_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> -Node Name: lodtest_optimized -Node Path: RootNode.lodtest_optimized +Node Name: lodtest_1_optimized +Node Path: RootNode.lodtest.lodtest_1_optimized Node Type: MeshData Positions: Count 24. Hash: 8661923109306356285 Normals: Count 24. Hash: 5807525742165000561 FaceList: Count 12. Hash: 9888799799190757436 FaceMaterialIds: Count 12. Hash: 7110546404675862471 -Node Name: lodtest_lod3_optimized -Node Path: RootNode.lodtest_lod3_optimized +Node Name: lodtest_lod3_1 +Node Path: RootNode.lodtest_lod3.lodtest_lod3_1 Node Type: MeshData Positions: Count 1984. Hash: 6600975913707260286 Normals: Count 1984. Hash: 2708036977889843831 FaceList: Count 960. Hash: 10390417165025722786 FaceMaterialIds: Count 960. Hash: 12510609185544665964 -Node Name: lodtest_lod2_optimized -Node Path: RootNode.lodtest_lod2_optimized +Node Name: lodtest_lod3_2 +Node Path: RootNode.lodtest_lod3.lodtest_lod3_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 2.298166, 0.000000> + +Node Name: lodtest_lod3_1_optimized +Node Path: RootNode.lodtest_lod3.lodtest_lod3_1_optimized +Node Type: MeshData + Positions: Count 1984. Hash: 6600975913707260286 + Normals: Count 1984. Hash: 2708036977889843831 + FaceList: Count 960. Hash: 10390417165025722786 + FaceMaterialIds: Count 960. Hash: 12510609185544665964 + +Node Name: lodtest_lod2_1 +Node Path: RootNode.lodtest_lod2.lodtest_lod2_1 Node Type: MeshData Positions: Count 240. Hash: 219362421205407416 Normals: Count 240. Hash: 11195242321181199939 FaceList: Count 80. Hash: 11130917988116538993 FaceMaterialIds: Count 80. Hash: 4190892684086530065 -Node Name: lodtest_lod1_optimized -Node Path: RootNode.lodtest_lod1_optimized +Node Name: lodtest_lod2_2 +Node Path: RootNode.lodtest_lod2.lodtest_lod2_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, -0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, -2.211498, 0.000000> + +Node Name: lodtest_lod2_1_optimized +Node Path: RootNode.lodtest_lod2.lodtest_lod2_1_optimized +Node Type: MeshData + Positions: Count 240. Hash: 219362421205407416 + Normals: Count 240. Hash: 11195242321181199939 + FaceList: Count 80. Hash: 11130917988116538993 + FaceMaterialIds: Count 80. Hash: 4190892684086530065 + +Node Name: lodtest_lod1_1 +Node Path: RootNode.lodtest_lod1.lodtest_lod1_1 +Node Type: MeshData + Positions: Count 192. Hash: 1283526254311745349 + Normals: Count 192. Hash: 1873340970602844856 + FaceList: Count 124. Hash: 3728991722746136013 + FaceMaterialIds: Count 124. Hash: 2372486708814455910 + +Node Name: lodtest_lod1_2 +Node Path: RootNode.lodtest_lod1.lodtest_lod1_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 2.410331, 0.000000, 0.000000> + +Node Name: lodtest_lod1_1_optimized +Node Path: RootNode.lodtest_lod1.lodtest_lod1_1_optimized Node Type: MeshData Positions: Count 192. Hash: 7921557352486854444 Normals: Count 192. Hash: 1873340970602844856 @@ -66,7 +111,7 @@ Node Type: MeshData FaceMaterialIds: Count 124. Hash: 2372486708814455910 Node Name: transform -Node Path: RootNode.lodtest.transform +Node Path: RootNode.lodtest.lodtest_1.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -75,13 +120,13 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.lodtest.UVMap +Node Path: RootNode.lodtest.lodtest_1.UVMap Node Type: MeshVertexUVData UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UVMap Node Name: Material -Node Path: RootNode.lodtest.Material +Node Path: RootNode.lodtest.lodtest_1.Material Node Type: MaterialData MaterialName: Material UniqueId: 11127505492038345244 @@ -110,45 +155,45 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.lodtest.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.lodtest.lodtest_1.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 24. Hash: 13438447437797057049 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.lodtest.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.lodtest.lodtest_1.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 24. Hash: 11372562338897179017 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.lodtest_lod3.transform +Node Path: RootNode.lodtest.lodtest_2.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> BasisY: < 0.000000, -0.000016, 100.000000> BasisZ: < 0.000000, -100.000000, -0.000016> - Transl: < 0.000000, 2.298166, 0.000000> + Transl: < 0.000000, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.lodtest_lod3.UVMap +Node Path: RootNode.lodtest.lodtest_2.UVMap Node Type: MeshVertexUVData - UVs: Count 1984. Hash: 14119273880200542497 + UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UVMap -Node Name: DefaultMaterial -Node Path: RootNode.lodtest_lod3.DefaultMaterial +Node Name: Material +Node Path: RootNode.lodtest.lodtest_2.Material Node Type: MaterialData - MaterialName: DefaultMaterial - UniqueId: 3809502407269006983 + MaterialName: Material + UniqueId: 11127505492038345244 IsNoDraw: false DiffuseColor: < 0.800000, 0.800000, 0.800000> - SpecularColor: < 0.000000, 0.000000, 0.000000> + SpecularColor: < 0.800000, 0.800000, 0.800000> EmissiveColor: < 0.000000, 0.000000, 0.000000> Opacity: 1.000000 - Shininess: 0.000000 + Shininess: 36.000000 UseColorMap: Not set BaseColor: Not set UseMetallicMap: Not set @@ -168,36 +213,81 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.lodtest_lod3.TangentSet_MikkT_0 +Node Name: UVMap +Node Path: RootNode.lodtest.lodtest_1_optimized.UVMap +Node Type: MeshVertexUVData + UVs: Count 24. Hash: 1622169145591646736 + UVCustomName: UVMap + +Node Name: TangentSet_0 +Node Path: RootNode.lodtest.lodtest_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData - Tangents: Count 1984. Hash: 5664494957869921957 - TangentSpace: 1 + Tangents: Count 24. Hash: 13438447437797057049 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.lodtest_lod3.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.lodtest.lodtest_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData - Bitangents: Count 1984. Hash: 5048878728906162461 - TangentSpace: 1 + Bitangents: Count 24. Hash: 11372562338897179017 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.lodtest_lod2.transform +Node Path: RootNode.lodtest.lodtest_1_optimized.transform Node Type: TransformData Matrix: - BasisX: < 100.000000, -0.000000, 0.000000> + BasisX: < 100.000000, 0.000000, 0.000000> BasisY: < 0.000000, -0.000016, 100.000000> BasisZ: < 0.000000, -100.000000, -0.000016> - Transl: < 0.000000, -2.211498, 0.000000> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: Material +Node Path: RootNode.lodtest.lodtest_1_optimized.Material +Node Type: MaterialData + MaterialName: Material + UniqueId: 11127505492038345244 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.800000, 0.800000, 0.800000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 36.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: + +Node Name: transform +Node Path: RootNode.lodtest_lod3.lodtest_lod3_1.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 2.298166, 0.000000> Node Name: UVMap -Node Path: RootNode.lodtest_lod2.UVMap +Node Path: RootNode.lodtest_lod3.lodtest_lod3_1.UVMap Node Type: MeshVertexUVData - UVs: Count 240. Hash: 13702273589593616598 + UVs: Count 1984. Hash: 14119273880200542497 UVCustomName: UVMap Node Name: DefaultMaterial -Node Path: RootNode.lodtest_lod2.DefaultMaterial +Node Path: RootNode.lodtest_lod3.lodtest_lod3_1.DefaultMaterial Node Type: MaterialData MaterialName: DefaultMaterial UniqueId: 3809502407269006983 @@ -226,36 +316,36 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.lodtest_lod2.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.lodtest_lod3.lodtest_lod3_1.TangentSet_0 Node Type: MeshVertexTangentData - Tangents: Count 240. Hash: 1390901212717410749 - TangentSpace: 1 + Tangents: Count 1984. Hash: 5664494957869921957 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.lodtest_lod2.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.lodtest_lod3.lodtest_lod3_1.BitangentSet_0 Node Type: MeshVertexBitangentData - Bitangents: Count 240. Hash: 1379238632949267281 - TangentSpace: 1 + Bitangents: Count 1984. Hash: 5048878728906162461 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.lodtest_lod1.transform +Node Path: RootNode.lodtest_lod3.lodtest_lod3_2.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> BasisY: < 0.000000, -0.000016, 100.000000> BasisZ: < 0.000000, -100.000000, -0.000016> - Transl: < 2.410331, 0.000000, 0.000000> + Transl: < 0.000000, 2.298166, 0.000000> Node Name: UVMap -Node Path: RootNode.lodtest_lod1.UVMap +Node Path: RootNode.lodtest_lod3.lodtest_lod3_2.UVMap Node Type: MeshVertexUVData - UVs: Count 192. Hash: 27253578623892681 + UVs: Count 1984. Hash: 14119273880200542497 UVCustomName: UVMap Node Name: DefaultMaterial -Node Path: RootNode.lodtest_lod1.DefaultMaterial +Node Path: RootNode.lodtest_lod3.lodtest_lod3_2.DefaultMaterial Node Type: MaterialData MaterialName: DefaultMaterial UniqueId: 3809502407269006983 @@ -284,58 +374,45 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.lodtest_lod1.TangentSet_MikkT_0 -Node Type: MeshVertexTangentData - Tangents: Count 192. Hash: 11165448242141781141 - TangentSpace: 1 - SetIndex: 0 - -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.lodtest_lod1.BitangentSet_MikkT_0 -Node Type: MeshVertexBitangentData - Bitangents: Count 192. Hash: 7987814487334449536 - TangentSpace: 1 - Node Name: UVMap -Node Path: RootNode.lodtest_optimized.UVMap +Node Path: RootNode.lodtest_lod3.lodtest_lod3_1_optimized.UVMap Node Type: MeshVertexUVData - UVs: Count 24. Hash: 1622169145591646736 + UVs: Count 1984. Hash: 14119273880200542497 UVCustomName: UVMap -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.lodtest_optimized.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.lodtest_lod3.lodtest_lod3_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData - Tangents: Count 24. Hash: 13438447437797057049 - TangentSpace: 1 + Tangents: Count 1984. Hash: 5664494957869921957 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.lodtest_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.lodtest_lod3.lodtest_lod3_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData - Bitangents: Count 24. Hash: 11372562338897179017 - TangentSpace: 1 + Bitangents: Count 1984. Hash: 5048878728906162461 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.lodtest_optimized.transform +Node Path: RootNode.lodtest_lod3.lodtest_lod3_1_optimized.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> BasisY: < 0.000000, -0.000016, 100.000000> BasisZ: < 0.000000, -100.000000, -0.000016> - Transl: < 0.000000, 0.000000, 0.000000> + Transl: < 0.000000, 2.298166, 0.000000> -Node Name: Material -Node Path: RootNode.lodtest_optimized.Material +Node Name: DefaultMaterial +Node Path: RootNode.lodtest_lod3.lodtest_lod3_1_optimized.DefaultMaterial Node Type: MaterialData - MaterialName: Material - UniqueId: 11127505492038345244 + MaterialName: DefaultMaterial + UniqueId: 3809502407269006983 IsNoDraw: false DiffuseColor: < 0.800000, 0.800000, 0.800000> - SpecularColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.000000, 0.000000, 0.000000> EmissiveColor: < 0.000000, 0.000000, 0.000000> Opacity: 1.000000 - Shininess: 36.000000 + Shininess: 0.000000 UseColorMap: Not set BaseColor: Not set UseMetallicMap: Not set @@ -355,36 +432,81 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: +Node Name: transform +Node Path: RootNode.lodtest_lod2.lodtest_lod2_1.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, -0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, -2.211498, 0.000000> + Node Name: UVMap -Node Path: RootNode.lodtest_lod3_optimized.UVMap +Node Path: RootNode.lodtest_lod2.lodtest_lod2_1.UVMap Node Type: MeshVertexUVData - UVs: Count 1984. Hash: 14119273880200542497 + UVs: Count 240. Hash: 13702273589593616598 UVCustomName: UVMap -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.lodtest_lod3_optimized.TangentSet_MikkT_0 +Node Name: DefaultMaterial +Node Path: RootNode.lodtest_lod2.lodtest_lod2_1.DefaultMaterial +Node Type: MaterialData + MaterialName: DefaultMaterial + UniqueId: 3809502407269006983 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.000000, 0.000000, 0.000000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 0.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: + +Node Name: TangentSet_0 +Node Path: RootNode.lodtest_lod2.lodtest_lod2_1.TangentSet_0 Node Type: MeshVertexTangentData - Tangents: Count 1984. Hash: 5664494957869921957 - TangentSpace: 1 + Tangents: Count 240. Hash: 1390901212717410749 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.lodtest_lod3_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.lodtest_lod2.lodtest_lod2_1.BitangentSet_0 Node Type: MeshVertexBitangentData - Bitangents: Count 1984. Hash: 5048878728906162461 - TangentSpace: 1 + Bitangents: Count 240. Hash: 1379238632949267281 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.lodtest_lod3_optimized.transform +Node Path: RootNode.lodtest_lod2.lodtest_lod2_2.transform Node Type: TransformData Matrix: - BasisX: < 100.000000, 0.000000, 0.000000> + BasisX: < 100.000000, -0.000000, 0.000000> BasisY: < 0.000000, -0.000016, 100.000000> BasisZ: < 0.000000, -100.000000, -0.000016> - Transl: < 0.000000, 2.298166, 0.000000> + Transl: < 0.000000, -2.211498, 0.000000> + +Node Name: UVMap +Node Path: RootNode.lodtest_lod2.lodtest_lod2_2.UVMap +Node Type: MeshVertexUVData + UVs: Count 240. Hash: 13702273589593616598 + UVCustomName: UVMap Node Name: DefaultMaterial -Node Path: RootNode.lodtest_lod3_optimized.DefaultMaterial +Node Path: RootNode.lodtest_lod2.lodtest_lod2_2.DefaultMaterial Node Type: MaterialData MaterialName: DefaultMaterial UniqueId: 3809502407269006983 @@ -414,26 +536,26 @@ Node Type: MaterialData BaseColorTexture: Node Name: UVMap -Node Path: RootNode.lodtest_lod2_optimized.UVMap +Node Path: RootNode.lodtest_lod2.lodtest_lod2_1_optimized.UVMap Node Type: MeshVertexUVData UVs: Count 240. Hash: 13702273589593616598 UVCustomName: UVMap -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.lodtest_lod2_optimized.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.lodtest_lod2.lodtest_lod2_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 240. Hash: 1390901212717410749 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.lodtest_lod2_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.lodtest_lod2.lodtest_lod2_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 240. Hash: 1379238632949267281 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.lodtest_lod2_optimized.transform +Node Path: RootNode.lodtest_lod2.lodtest_lod2_1_optimized.transform Node Type: TransformData Matrix: BasisX: < 100.000000, -0.000000, 0.000000> @@ -442,7 +564,7 @@ Node Type: TransformData Transl: < 0.000000, -2.211498, 0.000000> Node Name: DefaultMaterial -Node Path: RootNode.lodtest_lod2_optimized.DefaultMaterial +Node Path: RootNode.lodtest_lod2.lodtest_lod2_1_optimized.DefaultMaterial Node Type: MaterialData MaterialName: DefaultMaterial UniqueId: 3809502407269006983 @@ -471,27 +593,130 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: +Node Name: transform +Node Path: RootNode.lodtest_lod1.lodtest_lod1_1.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 2.410331, 0.000000, 0.000000> + Node Name: UVMap -Node Path: RootNode.lodtest_lod1_optimized.UVMap +Node Path: RootNode.lodtest_lod1.lodtest_lod1_1.UVMap +Node Type: MeshVertexUVData + UVs: Count 192. Hash: 27253578623892681 + UVCustomName: UVMap + +Node Name: DefaultMaterial +Node Path: RootNode.lodtest_lod1.lodtest_lod1_1.DefaultMaterial +Node Type: MaterialData + MaterialName: DefaultMaterial + UniqueId: 3809502407269006983 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.000000, 0.000000, 0.000000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 0.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: + +Node Name: TangentSet_0 +Node Path: RootNode.lodtest_lod1.lodtest_lod1_1.TangentSet_0 +Node Type: MeshVertexTangentData + Tangents: Count 192. Hash: 11165448242141781141 + GenerationMethod: 1 + SetIndex: 0 + +Node Name: BitangentSet_0 +Node Path: RootNode.lodtest_lod1.lodtest_lod1_1.BitangentSet_0 +Node Type: MeshVertexBitangentData + Bitangents: Count 192. Hash: 7987814487334449536 + GenerationMethod: 1 + +Node Name: transform +Node Path: RootNode.lodtest_lod1.lodtest_lod1_2.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 2.410331, 0.000000, 0.000000> + +Node Name: UVMap +Node Path: RootNode.lodtest_lod1.lodtest_lod1_2.UVMap +Node Type: MeshVertexUVData + UVs: Count 192. Hash: 27253578623892681 + UVCustomName: UVMap + +Node Name: DefaultMaterial +Node Path: RootNode.lodtest_lod1.lodtest_lod1_2.DefaultMaterial +Node Type: MaterialData + MaterialName: DefaultMaterial + UniqueId: 3809502407269006983 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.000000, 0.000000, 0.000000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 0.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: + +Node Name: UVMap +Node Path: RootNode.lodtest_lod1.lodtest_lod1_1_optimized.UVMap Node Type: MeshVertexUVData UVs: Count 192. Hash: 13790301632763350589 UVCustomName: UVMap -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.lodtest_lod1_optimized.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.lodtest_lod1.lodtest_lod1_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 192. Hash: 7293001660047850407 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.lodtest_lod1_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.lodtest_lod1.lodtest_lod1_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 192. Hash: 2874689498270494796 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.lodtest_lod1_optimized.transform +Node Path: RootNode.lodtest_lod1.lodtest_lod1_1_optimized.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -500,7 +725,7 @@ Node Type: TransformData Transl: < 2.410331, 0.000000, 0.000000> Node Name: DefaultMaterial -Node Path: RootNode.lodtest_lod1_optimized.DefaultMaterial +Node Path: RootNode.lodtest_lod1.lodtest_lod1_1_optimized.DefaultMaterial Node Type: MaterialData MaterialName: DefaultMaterial UniqueId: 3809502407269006983 @@ -528,4 +753,3 @@ Node Type: MaterialData AmbientOcclusionTexture: EmissiveTexture: BaseColorTexture: - diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/SoftNamingPhysics/physicstest.dbgsg b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/SoftNamingPhysics/physicstest.dbgsg index f620b6c35b..9b7ec58b7a 100644 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/SoftNamingPhysics/physicstest.dbgsg +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/SoftNamingPhysics/physicstest.dbgsg @@ -1,32 +1,59 @@ ProductName: physicstest.dbgsg debugSceneGraphVersion: 1 physicstest -Node Name: Cone -Node Path: RootNode.Cone +Node Name: RootNode +Node Path: RootNode +Node Type: RootBoneData + WorldTransform: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: Cone_1 +Node Path: RootNode.Cone.Cone_1 Node Type: MeshData Positions: Count 128. Hash: 7714223793259938211 Normals: Count 128. Hash: 2352668179264002707 FaceList: Count 62. Hash: 14563017593520122982 FaceMaterialIds: Count 62. Hash: 12234218120113875284 -Node Name: Cube_phys -Node Path: RootNode.Cube_phys -Node Type: MeshData - Positions: Count 24. Hash: 3478903613105670818 - Normals: Count 24. Hash: 7251512570672401149 - FaceList: Count 12. Hash: 9888799799190757436 - FaceMaterialIds: Count 12. Hash: 7110546404675862471 +Node Name: Cone_2 +Node Path: RootNode.Cone.Cone_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> -Node Name: Cone_optimized -Node Path: RootNode.Cone_optimized +Node Name: Cone_1_optimized +Node Path: RootNode.Cone.Cone_1_optimized Node Type: MeshData Positions: Count 128. Hash: 10174710861731544050 Normals: Count 128. Hash: 2352668179264002707 FaceList: Count 62. Hash: 11332459830831720586 FaceMaterialIds: Count 62. Hash: 12234218120113875284 +Node Name: Cube_phys_1 +Node Path: RootNode.Cube_phys.Cube_phys_1 +Node Type: MeshData + Positions: Count 24. Hash: 3478903613105670818 + Normals: Count 24. Hash: 7251512570672401149 + FaceList: Count 12. Hash: 9888799799190757436 + FaceMaterialIds: Count 12. Hash: 7110546404675862471 + +Node Name: Cube_phys_2 +Node Path: RootNode.Cube_phys.Cube_phys_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> + Node Name: transform -Node Path: RootNode.Cone.transform +Node Path: RootNode.Cone.Cone_1.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -35,13 +62,13 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.Cone.UVMap +Node Path: RootNode.Cone.Cone_1.UVMap Node Type: MeshVertexUVData UVs: Count 128. Hash: 10171083346831193808 UVCustomName: UVMap Node Name: DefaultMaterial -Node Path: RootNode.Cone.DefaultMaterial +Node Path: RootNode.Cone.Cone_1.DefaultMaterial Node Type: MaterialData MaterialName: DefaultMaterial UniqueId: 3809502407269006983 @@ -70,21 +97,21 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cone.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Cone.Cone_1.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 128. Hash: 14351734474754285313 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cone.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cone.Cone_1.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 128. Hash: 15997251922861304891 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Cube_phys.transform +Node Path: RootNode.Cone.Cone_2.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -93,13 +120,13 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.Cube_phys.UVMap +Node Path: RootNode.Cone.Cone_2.UVMap Node Type: MeshVertexUVData - UVs: Count 24. Hash: 13623018071435219250 + UVs: Count 128. Hash: 10171083346831193808 UVCustomName: UVMap Node Name: DefaultMaterial -Node Path: RootNode.Cube_phys.DefaultMaterial +Node Path: RootNode.Cone.Cone_2.DefaultMaterial Node Type: MaterialData MaterialName: DefaultMaterial UniqueId: 3809502407269006983 @@ -128,40 +155,124 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cube_phys.TangentSet_MikkT_0 +Node Name: UVMap +Node Path: RootNode.Cone.Cone_1_optimized.UVMap +Node Type: MeshVertexUVData + UVs: Count 128. Hash: 7873368003484215433 + UVCustomName: UVMap + +Node Name: TangentSet_0 +Node Path: RootNode.Cone.Cone_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData - Tangents: Count 24. Hash: 11965897353301448436 - TangentSpace: 1 + Tangents: Count 128. Hash: 12937806066914201637 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cube_phys.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cone.Cone_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData - Bitangents: Count 24. Hash: 17515781720544086759 - TangentSpace: 1 + Bitangents: Count 128. Hash: 873786942732834087 + GenerationMethod: 1 + +Node Name: transform +Node Path: RootNode.Cone.Cone_1_optimized.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: DefaultMaterial +Node Path: RootNode.Cone.Cone_1_optimized.DefaultMaterial +Node Type: MaterialData + MaterialName: DefaultMaterial + UniqueId: 3809502407269006983 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.000000, 0.000000, 0.000000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 0.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: + +Node Name: transform +Node Path: RootNode.Cube_phys.Cube_phys_1.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.Cone_optimized.UVMap +Node Path: RootNode.Cube_phys.Cube_phys_1.UVMap Node Type: MeshVertexUVData - UVs: Count 128. Hash: 7873368003484215433 + UVs: Count 24. Hash: 13623018071435219250 UVCustomName: UVMap -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cone_optimized.TangentSet_MikkT_0 +Node Name: DefaultMaterial +Node Path: RootNode.Cube_phys.Cube_phys_1.DefaultMaterial +Node Type: MaterialData + MaterialName: DefaultMaterial + UniqueId: 3809502407269006983 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.000000, 0.000000, 0.000000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 0.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: + +Node Name: TangentSet_0 +Node Path: RootNode.Cube_phys.Cube_phys_1.TangentSet_0 Node Type: MeshVertexTangentData - Tangents: Count 128. Hash: 12937806066914201637 - TangentSpace: 1 + Tangents: Count 24. Hash: 11965897353301448436 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cone_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cube_phys.Cube_phys_1.BitangentSet_0 Node Type: MeshVertexBitangentData - Bitangents: Count 128. Hash: 873786942732834087 - TangentSpace: 1 + Bitangents: Count 24. Hash: 17515781720544086759 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Cone_optimized.transform +Node Path: RootNode.Cube_phys.Cube_phys_2.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -169,8 +280,14 @@ Node Type: TransformData BasisZ: < 0.000000, -100.000000, -0.000016> Transl: < 0.000000, 0.000000, 0.000000> +Node Name: UVMap +Node Path: RootNode.Cube_phys.Cube_phys_2.UVMap +Node Type: MeshVertexUVData + UVs: Count 24. Hash: 13623018071435219250 + UVCustomName: UVMap + Node Name: DefaultMaterial -Node Path: RootNode.Cone_optimized.DefaultMaterial +Node Path: RootNode.Cube_phys.Cube_phys_2.DefaultMaterial Node Type: MaterialData MaterialName: DefaultMaterial UniqueId: 3809502407269006983 @@ -198,4 +315,3 @@ Node Type: MaterialData AmbientOcclusionTexture: EmissiveTexture: BaseColorTexture: - diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshLinkedMaterials/multiple_mesh_linked_materials.dbgsg b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshLinkedMaterials/multiple_mesh_linked_materials.dbgsg index 50f9285712..c0544ea3a9 100644 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshLinkedMaterials/multiple_mesh_linked_materials.dbgsg +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshLinkedMaterials/multiple_mesh_linked_materials.dbgsg @@ -1,32 +1,59 @@ ProductName: multiple_mesh_linked_materials.dbgsg debugSceneGraphVersion: 1 multiple_mesh_linked_materials -Node Name: Cube -Node Path: RootNode.Cube +Node Name: RootNode +Node Path: RootNode +Node Type: RootBoneData + WorldTransform: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: Cube_1 +Node Path: RootNode.Cube.Cube_1 Node Type: MeshData Positions: Count 24. Hash: 8661923109306356285 Normals: Count 24. Hash: 5807525742165000561 FaceList: Count 12. Hash: 9888799799190757436 FaceMaterialIds: Count 12. Hash: 7113802799051126666 -Node Name: Cone -Node Path: RootNode.Cone -Node Type: MeshData - Positions: Count 128. Hash: 12506421592104186200 - Normals: Count 128. Hash: 367461522682321485 - FaceList: Count 62. Hash: 13208951979626973193 - FaceMaterialIds: Count 62. Hash: 15454348664434923102 +Node Name: Cube_2 +Node Path: RootNode.Cube.Cube_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> -Node Name: Cube_optimized -Node Path: RootNode.Cube_optimized +Node Name: Cube_1_optimized +Node Path: RootNode.Cube.Cube_1_optimized Node Type: MeshData Positions: Count 24. Hash: 8661923109306356285 Normals: Count 24. Hash: 5807525742165000561 FaceList: Count 12. Hash: 9888799799190757436 FaceMaterialIds: Count 12. Hash: 7113802799051126666 -Node Name: Cone_optimized -Node Path: RootNode.Cone_optimized +Node Name: Cone_1 +Node Path: RootNode.Cone.Cone_1 +Node Type: MeshData + Positions: Count 128. Hash: 12506421592104186200 + Normals: Count 128. Hash: 367461522682321485 + FaceList: Count 62. Hash: 13208951979626973193 + FaceMaterialIds: Count 62. Hash: 15454348664434923102 + +Node Name: Cone_2 +Node Path: RootNode.Cone.Cone_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 2.000000> + +Node Name: Cone_1_optimized +Node Path: RootNode.Cone.Cone_1_optimized Node Type: MeshData Positions: Count 128. Hash: 14946490408303214595 Normals: Count 128. Hash: 367461522682321485 @@ -34,7 +61,7 @@ Node Type: MeshData FaceMaterialIds: Count 62. Hash: 15454348664434923102 Node Name: transform -Node Path: RootNode.Cube.transform +Node Path: RootNode.Cube.Cube_1.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -43,13 +70,13 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: UV0 -Node Path: RootNode.Cube.UV0 +Node Path: RootNode.Cube.Cube_1.UV0 Node Type: MeshVertexUVData UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UV0 Node Name: SharedBlack -Node Path: RootNode.Cube.SharedBlack +Node Path: RootNode.Cube.Cube_1.SharedBlack Node Type: MaterialData MaterialName: SharedBlack UniqueId: 5248829540156873090 @@ -79,7 +106,7 @@ Node Type: MaterialData BaseColorTexture: Node Name: SharedOrange -Node Path: RootNode.Cube.SharedOrange +Node Path: RootNode.Cube.Cube_1.SharedOrange Node Type: MaterialData MaterialName: SharedOrange UniqueId: 9470651048605569128 @@ -108,42 +135,42 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cube.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Cube.Cube_1.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 24. Hash: 13438447437797057049 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cube.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cube.Cube_1.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 24. Hash: 11372562338897179017 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Cone.transform +Node Path: RootNode.Cube.Cube_2.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> BasisY: < 0.000000, -0.000016, 100.000000> BasisZ: < 0.000000, -100.000000, -0.000016> - Transl: < 0.000000, 0.000000, 2.000000> + Transl: < 0.000000, 0.000000, 0.000000> Node Name: UV0 -Node Path: RootNode.Cone.UV0 +Node Path: RootNode.Cube.Cube_2.UV0 Node Type: MeshVertexUVData - UVs: Count 128. Hash: 10291654057525777310 + UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UV0 -Node Name: SharedOrange -Node Path: RootNode.Cone.SharedOrange +Node Name: SharedBlack +Node Path: RootNode.Cube.Cube_2.SharedBlack Node Type: MaterialData - MaterialName: SharedOrange - UniqueId: 9470651048605569128 + MaterialName: SharedBlack + UniqueId: 5248829540156873090 IsNoDraw: false - DiffuseColor: < 0.800000, 0.139382, 0.014429> - SpecularColor: < 0.800000, 0.139382, 0.014429> + DiffuseColor: < 0.000000, 0.000000, 0.000000> + SpecularColor: < 0.000000, 0.000000, 0.000000> EmissiveColor: < 0.000000, 0.000000, 0.000000> Opacity: 1.000000 Shininess: 25.000000 @@ -166,14 +193,14 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: -Node Name: SharedBlack -Node Path: RootNode.Cone.SharedBlack +Node Name: SharedOrange +Node Path: RootNode.Cube.Cube_2.SharedOrange Node Type: MaterialData - MaterialName: SharedBlack - UniqueId: 5248829540156873090 + MaterialName: SharedOrange + UniqueId: 9470651048605569128 IsNoDraw: false - DiffuseColor: < 0.000000, 0.000000, 0.000000> - SpecularColor: < 0.000000, 0.000000, 0.000000> + DiffuseColor: < 0.800000, 0.139382, 0.014429> + SpecularColor: < 0.800000, 0.139382, 0.014429> EmissiveColor: < 0.000000, 0.000000, 0.000000> Opacity: 1.000000 Shininess: 25.000000 @@ -196,40 +223,27 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cone.TangentSet_MikkT_0 -Node Type: MeshVertexTangentData - Tangents: Count 128. Hash: 12695232913942738512 - TangentSpace: 1 - SetIndex: 0 - -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cone.BitangentSet_MikkT_0 -Node Type: MeshVertexBitangentData - Bitangents: Count 128. Hash: 9034210764777745751 - TangentSpace: 1 - Node Name: UV0 -Node Path: RootNode.Cube_optimized.UV0 +Node Path: RootNode.Cube.Cube_1_optimized.UV0 Node Type: MeshVertexUVData UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UV0 -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cube_optimized.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Cube.Cube_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 24. Hash: 13438447437797057049 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cube_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cube.Cube_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 24. Hash: 11372562338897179017 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Cube_optimized.transform +Node Path: RootNode.Cube.Cube_1_optimized.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -238,7 +252,7 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: SharedBlack -Node Path: RootNode.Cube_optimized.SharedBlack +Node Path: RootNode.Cube.Cube_1_optimized.SharedBlack Node Type: MaterialData MaterialName: SharedBlack UniqueId: 5248829540156873090 @@ -268,7 +282,7 @@ Node Type: MaterialData BaseColorTexture: Node Name: SharedOrange -Node Path: RootNode.Cube_optimized.SharedOrange +Node Path: RootNode.Cube.Cube_1_optimized.SharedOrange Node Type: MaterialData MaterialName: SharedOrange UniqueId: 9470651048605569128 @@ -297,27 +311,190 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: +Node Name: transform +Node Path: RootNode.Cone.Cone_1.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 2.000000> + Node Name: UV0 -Node Path: RootNode.Cone_optimized.UV0 +Node Path: RootNode.Cone.Cone_1.UV0 +Node Type: MeshVertexUVData + UVs: Count 128. Hash: 10291654057525777310 + UVCustomName: UV0 + +Node Name: SharedOrange +Node Path: RootNode.Cone.Cone_1.SharedOrange +Node Type: MaterialData + MaterialName: SharedOrange + UniqueId: 9470651048605569128 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.139382, 0.014429> + SpecularColor: < 0.800000, 0.139382, 0.014429> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 25.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: + +Node Name: SharedBlack +Node Path: RootNode.Cone.Cone_1.SharedBlack +Node Type: MaterialData + MaterialName: SharedBlack + UniqueId: 5248829540156873090 + IsNoDraw: false + DiffuseColor: < 0.000000, 0.000000, 0.000000> + SpecularColor: < 0.000000, 0.000000, 0.000000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 25.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: + +Node Name: TangentSet_0 +Node Path: RootNode.Cone.Cone_1.TangentSet_0 +Node Type: MeshVertexTangentData + Tangents: Count 128. Hash: 12695232913942738512 + GenerationMethod: 1 + SetIndex: 0 + +Node Name: BitangentSet_0 +Node Path: RootNode.Cone.Cone_1.BitangentSet_0 +Node Type: MeshVertexBitangentData + Bitangents: Count 128. Hash: 9034210764777745751 + GenerationMethod: 1 + +Node Name: transform +Node Path: RootNode.Cone.Cone_2.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 2.000000> + +Node Name: UV0 +Node Path: RootNode.Cone.Cone_2.UV0 +Node Type: MeshVertexUVData + UVs: Count 128. Hash: 10291654057525777310 + UVCustomName: UV0 + +Node Name: SharedOrange +Node Path: RootNode.Cone.Cone_2.SharedOrange +Node Type: MaterialData + MaterialName: SharedOrange + UniqueId: 9470651048605569128 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.139382, 0.014429> + SpecularColor: < 0.800000, 0.139382, 0.014429> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 25.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: + +Node Name: SharedBlack +Node Path: RootNode.Cone.Cone_2.SharedBlack +Node Type: MaterialData + MaterialName: SharedBlack + UniqueId: 5248829540156873090 + IsNoDraw: false + DiffuseColor: < 0.000000, 0.000000, 0.000000> + SpecularColor: < 0.000000, 0.000000, 0.000000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 25.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: + +Node Name: UV0 +Node Path: RootNode.Cone.Cone_1_optimized.UV0 Node Type: MeshVertexUVData UVs: Count 128. Hash: 7173974213247584731 UVCustomName: UV0 -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cone_optimized.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Cone.Cone_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 128. Hash: 10740776669168782230 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cone_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cone.Cone_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 128. Hash: 6990068477421150065 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Cone_optimized.transform +Node Path: RootNode.Cone.Cone_1_optimized.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -326,7 +503,7 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 2.000000> Node Name: SharedOrange -Node Path: RootNode.Cone_optimized.SharedOrange +Node Path: RootNode.Cone.Cone_1_optimized.SharedOrange Node Type: MaterialData MaterialName: SharedOrange UniqueId: 9470651048605569128 @@ -356,7 +533,7 @@ Node Type: MaterialData BaseColorTexture: Node Name: SharedBlack -Node Path: RootNode.Cone_optimized.SharedBlack +Node Path: RootNode.Cone.Cone_1_optimized.SharedBlack Node Type: MaterialData MaterialName: SharedBlack UniqueId: 5248829540156873090 @@ -384,4 +561,3 @@ Node Type: MaterialData AmbientOcclusionTexture: EmissiveTexture: BaseColorTexture: - diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshOneMaterial/multiple_mesh_one_material.dbgsg b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshOneMaterial/multiple_mesh_one_material.dbgsg index d341bcdb70..48135760f2 100644 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshOneMaterial/multiple_mesh_one_material.dbgsg +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshOneMaterial/multiple_mesh_one_material.dbgsg @@ -1,32 +1,59 @@ ProductName: multiple_mesh_one_material.dbgsg debugSceneGraphVersion: 1 multiple_mesh_one_material -Node Name: Cube -Node Path: RootNode.Cube +Node Name: RootNode +Node Path: RootNode +Node Type: RootBoneData + WorldTransform: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: Cube_1 +Node Path: RootNode.Cube.Cube_1 Node Type: MeshData Positions: Count 24. Hash: 8661923109306356285 Normals: Count 24. Hash: 5807525742165000561 FaceList: Count 12. Hash: 9888799799190757436 FaceMaterialIds: Count 12. Hash: 7110546404675862471 -Node Name: Cylinder -Node Path: RootNode.Cylinder -Node Type: MeshData - Positions: Count 192. Hash: 1283526254311745349 - Normals: Count 192. Hash: 1873340970602844856 - FaceList: Count 124. Hash: 3728991722746136013 - FaceMaterialIds: Count 124. Hash: 2372486708814455910 +Node Name: Cube_2 +Node Path: RootNode.Cube.Cube_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> -Node Name: Cube_optimized -Node Path: RootNode.Cube_optimized +Node Name: Cube_1_optimized +Node Path: RootNode.Cube.Cube_1_optimized Node Type: MeshData Positions: Count 24. Hash: 8661923109306356285 Normals: Count 24. Hash: 5807525742165000561 FaceList: Count 12. Hash: 9888799799190757436 FaceMaterialIds: Count 12. Hash: 7110546404675862471 -Node Name: Cylinder_optimized -Node Path: RootNode.Cylinder_optimized +Node Name: Cylinder_1 +Node Path: RootNode.Cylinder.Cylinder_1 +Node Type: MeshData + Positions: Count 192. Hash: 1283526254311745349 + Normals: Count 192. Hash: 1873340970602844856 + FaceList: Count 124. Hash: 3728991722746136013 + FaceMaterialIds: Count 124. Hash: 2372486708814455910 + +Node Name: Cylinder_2 +Node Path: RootNode.Cylinder.Cylinder_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: <-4.388482, 0.000000, 0.000000> + +Node Name: Cylinder_1_optimized +Node Path: RootNode.Cylinder.Cylinder_1_optimized Node Type: MeshData Positions: Count 192. Hash: 7921557352486854444 Normals: Count 192. Hash: 1873340970602844856 @@ -34,7 +61,7 @@ Node Type: MeshData FaceMaterialIds: Count 124. Hash: 2372486708814455910 Node Name: transform -Node Path: RootNode.Cube.transform +Node Path: RootNode.Cube.Cube_1.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -43,13 +70,13 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.Cube.UVMap +Node Path: RootNode.Cube.Cube_1.UVMap Node Type: MeshVertexUVData UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UVMap Node Name: SingleMaterial -Node Path: RootNode.Cube.SingleMaterial +Node Path: RootNode.Cube.Cube_1.SingleMaterial Node Type: MaterialData MaterialName: SingleMaterial UniqueId: 14432700632681398127 @@ -78,36 +105,36 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: TwoMeshOneMaterial/FBXTestTexture.png -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cube.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Cube.Cube_1.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 24. Hash: 13438447437797057049 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cube.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cube.Cube_1.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 24. Hash: 11372562338897179017 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Cylinder.transform +Node Path: RootNode.Cube.Cube_2.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> BasisY: < 0.000000, -0.000016, 100.000000> BasisZ: < 0.000000, -100.000000, -0.000016> - Transl: <-4.388482, 0.000000, 0.000000> + Transl: < 0.000000, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.Cylinder.UVMap +Node Path: RootNode.Cube.Cube_2.UVMap Node Type: MeshVertexUVData - UVs: Count 192. Hash: 27253578623892681 + UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UVMap Node Name: SingleMaterial -Node Path: RootNode.Cylinder.SingleMaterial +Node Path: RootNode.Cube.Cube_2.SingleMaterial Node Type: MaterialData MaterialName: SingleMaterial UniqueId: 14432700632681398127 @@ -136,49 +163,139 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: TwoMeshOneMaterial/FBXTestTexture.png -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cylinder.TangentSet_MikkT_0 +Node Name: UVMap +Node Path: RootNode.Cube.Cube_1_optimized.UVMap +Node Type: MeshVertexUVData + UVs: Count 24. Hash: 1622169145591646736 + UVCustomName: UVMap + +Node Name: TangentSet_0 +Node Path: RootNode.Cube.Cube_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData - Tangents: Count 192. Hash: 11165448242141781141 - TangentSpace: 1 + Tangents: Count 24. Hash: 13438447437797057049 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cylinder.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cube.Cube_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData - Bitangents: Count 192. Hash: 7987814487334449536 - TangentSpace: 1 + Bitangents: Count 24. Hash: 11372562338897179017 + GenerationMethod: 1 + +Node Name: transform +Node Path: RootNode.Cube.Cube_1_optimized.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: SingleMaterial +Node Path: RootNode.Cube.Cube_1_optimized.SingleMaterial +Node Type: MaterialData + MaterialName: SingleMaterial + UniqueId: 14432700632681398127 + IsNoDraw: false + DiffuseColor: < 0.814049, 0.814049, 0.814049> + SpecularColor: < 0.814049, 0.814049, 0.814049> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 25.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: TwoMeshOneMaterial/FBXTestTexture.png + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: TwoMeshOneMaterial/FBXTestTexture.png + +Node Name: transform +Node Path: RootNode.Cylinder.Cylinder_1.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: <-4.388482, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.Cube_optimized.UVMap +Node Path: RootNode.Cylinder.Cylinder_1.UVMap Node Type: MeshVertexUVData - UVs: Count 24. Hash: 1622169145591646736 + UVs: Count 192. Hash: 27253578623892681 UVCustomName: UVMap -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cube_optimized.TangentSet_MikkT_0 +Node Name: SingleMaterial +Node Path: RootNode.Cylinder.Cylinder_1.SingleMaterial +Node Type: MaterialData + MaterialName: SingleMaterial + UniqueId: 14432700632681398127 + IsNoDraw: false + DiffuseColor: < 0.814049, 0.814049, 0.814049> + SpecularColor: < 0.814049, 0.814049, 0.814049> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 25.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: TwoMeshOneMaterial/FBXTestTexture.png + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: TwoMeshOneMaterial/FBXTestTexture.png + +Node Name: TangentSet_0 +Node Path: RootNode.Cylinder.Cylinder_1.TangentSet_0 Node Type: MeshVertexTangentData - Tangents: Count 24. Hash: 13438447437797057049 - TangentSpace: 1 + Tangents: Count 192. Hash: 11165448242141781141 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cube_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cylinder.Cylinder_1.BitangentSet_0 Node Type: MeshVertexBitangentData - Bitangents: Count 24. Hash: 11372562338897179017 - TangentSpace: 1 + Bitangents: Count 192. Hash: 7987814487334449536 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Cube_optimized.transform +Node Path: RootNode.Cylinder.Cylinder_2.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> BasisY: < 0.000000, -0.000016, 100.000000> BasisZ: < 0.000000, -100.000000, -0.000016> - Transl: < 0.000000, 0.000000, 0.000000> + Transl: <-4.388482, 0.000000, 0.000000> + +Node Name: UVMap +Node Path: RootNode.Cylinder.Cylinder_2.UVMap +Node Type: MeshVertexUVData + UVs: Count 192. Hash: 27253578623892681 + UVCustomName: UVMap Node Name: SingleMaterial -Node Path: RootNode.Cube_optimized.SingleMaterial +Node Path: RootNode.Cylinder.Cylinder_2.SingleMaterial Node Type: MaterialData MaterialName: SingleMaterial UniqueId: 14432700632681398127 @@ -208,26 +325,26 @@ Node Type: MaterialData BaseColorTexture: TwoMeshOneMaterial/FBXTestTexture.png Node Name: UVMap -Node Path: RootNode.Cylinder_optimized.UVMap +Node Path: RootNode.Cylinder.Cylinder_1_optimized.UVMap Node Type: MeshVertexUVData UVs: Count 192. Hash: 13790301632763350589 UVCustomName: UVMap -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cylinder_optimized.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Cylinder.Cylinder_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 192. Hash: 7293001660047850407 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cylinder_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cylinder.Cylinder_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 192. Hash: 2874689498270494796 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Cylinder_optimized.transform +Node Path: RootNode.Cylinder.Cylinder_1_optimized.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -236,7 +353,7 @@ Node Type: TransformData Transl: <-4.388482, 0.000000, 0.000000> Node Name: SingleMaterial -Node Path: RootNode.Cylinder_optimized.SingleMaterial +Node Path: RootNode.Cylinder.Cylinder_1_optimized.SingleMaterial Node Type: MaterialData MaterialName: SingleMaterial UniqueId: 14432700632681398127 @@ -264,4 +381,3 @@ Node Type: MaterialData AmbientOcclusionTexture: EmissiveTexture: BaseColorTexture: TwoMeshOneMaterial/FBXTestTexture.png - diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshTwoMaterial/multiple_mesh_multiple_material.dbgsg b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshTwoMaterial/multiple_mesh_multiple_material.dbgsg index b2d8df6f4a..c5587a2778 100644 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshTwoMaterial/multiple_mesh_multiple_material.dbgsg +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshTwoMaterial/multiple_mesh_multiple_material.dbgsg @@ -1,32 +1,59 @@ ProductName: multiple_mesh_multiple_material.dbgsg debugSceneGraphVersion: 1 multiple_mesh_multiple_material -Node Name: Cube -Node Path: RootNode.Cube +Node Name: RootNode +Node Path: RootNode +Node Type: RootBoneData + WorldTransform: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: Cube_1 +Node Path: RootNode.Cube.Cube_1 Node Type: MeshData Positions: Count 24. Hash: 8661923109306356285 Normals: Count 24. Hash: 5807525742165000561 FaceList: Count 12. Hash: 9888799799190757436 FaceMaterialIds: Count 12. Hash: 7110546404675862471 -Node Name: Cylinder -Node Path: RootNode.Cylinder -Node Type: MeshData - Positions: Count 192. Hash: 1283526254311745349 - Normals: Count 192. Hash: 1873340970602844856 - FaceList: Count 124. Hash: 3728991722746136013 - FaceMaterialIds: Count 124. Hash: 2372486708814455910 +Node Name: Cube_2 +Node Path: RootNode.Cube.Cube_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> -Node Name: Cube_optimized -Node Path: RootNode.Cube_optimized +Node Name: Cube_1_optimized +Node Path: RootNode.Cube.Cube_1_optimized Node Type: MeshData Positions: Count 24. Hash: 8661923109306356285 Normals: Count 24. Hash: 5807525742165000561 FaceList: Count 12. Hash: 9888799799190757436 FaceMaterialIds: Count 12. Hash: 7110546404675862471 -Node Name: Cylinder_optimized -Node Path: RootNode.Cylinder_optimized +Node Name: Cylinder_1 +Node Path: RootNode.Cylinder.Cylinder_1 +Node Type: MeshData + Positions: Count 192. Hash: 1283526254311745349 + Normals: Count 192. Hash: 1873340970602844856 + FaceList: Count 124. Hash: 3728991722746136013 + FaceMaterialIds: Count 124. Hash: 2372486708814455910 + +Node Name: Cylinder_2 +Node Path: RootNode.Cylinder.Cylinder_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: <-4.388482, 0.000000, 0.000000> + +Node Name: Cylinder_1_optimized +Node Path: RootNode.Cylinder.Cylinder_1_optimized Node Type: MeshData Positions: Count 192. Hash: 7921557352486854444 Normals: Count 192. Hash: 1873340970602844856 @@ -34,7 +61,7 @@ Node Type: MeshData FaceMaterialIds: Count 124. Hash: 2372486708814455910 Node Name: transform -Node Path: RootNode.Cube.transform +Node Path: RootNode.Cube.Cube_1.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -43,13 +70,13 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.Cube.UVMap +Node Path: RootNode.Cube.Cube_1.UVMap Node Type: MeshVertexUVData UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UVMap Node Name: SingleMaterial -Node Path: RootNode.Cube.SingleMaterial +Node Path: RootNode.Cube.Cube_1.SingleMaterial Node Type: MaterialData MaterialName: SingleMaterial UniqueId: 14432700632681398127 @@ -78,42 +105,42 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: TwoMeshTwoMaterial/FBXTestTexture.png -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cube.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Cube.Cube_1.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 24. Hash: 13438447437797057049 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cube.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cube.Cube_1.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 24. Hash: 11372562338897179017 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Cylinder.transform +Node Path: RootNode.Cube.Cube_2.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> BasisY: < 0.000000, -0.000016, 100.000000> BasisZ: < 0.000000, -100.000000, -0.000016> - Transl: <-4.388482, 0.000000, 0.000000> + Transl: < 0.000000, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.Cylinder.UVMap +Node Path: RootNode.Cube.Cube_2.UVMap Node Type: MeshVertexUVData - UVs: Count 192. Hash: 27253578623892681 + UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UVMap -Node Name: SecondMaterial -Node Path: RootNode.Cylinder.SecondMaterial +Node Name: SingleMaterial +Node Path: RootNode.Cube.Cube_2.SingleMaterial Node Type: MaterialData - MaterialName: SecondMaterial - UniqueId: 5229255358802505087 + MaterialName: SingleMaterial + UniqueId: 14432700632681398127 IsNoDraw: false - DiffuseColor: < 0.800000, 0.800000, 0.800000> - SpecularColor: < 0.800000, 0.800000, 0.800000> + DiffuseColor: < 0.814049, 0.814049, 0.814049> + SpecularColor: < 0.814049, 0.814049, 0.814049> EmissiveColor: < 0.000000, 0.000000, 0.000000> Opacity: 1.000000 Shininess: 25.000000 @@ -126,7 +153,7 @@ Node Type: MaterialData UseEmissiveMap: Not set EmissiveIntensity: Not set UseAOMap: Not set - DiffuseTexture: TwoMeshTwoMaterial/FBXSecondTestTexture.png + DiffuseTexture: TwoMeshTwoMaterial/FBXTestTexture.png SpecularTexture: BumpTexture: NormalTexture: @@ -134,42 +161,29 @@ Node Type: MaterialData RoughnessTexture: AmbientOcclusionTexture: EmissiveTexture: - BaseColorTexture: TwoMeshTwoMaterial/FBXSecondTestTexture.png - -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cylinder.TangentSet_MikkT_0 -Node Type: MeshVertexTangentData - Tangents: Count 192. Hash: 11165448242141781141 - TangentSpace: 1 - SetIndex: 0 - -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cylinder.BitangentSet_MikkT_0 -Node Type: MeshVertexBitangentData - Bitangents: Count 192. Hash: 7987814487334449536 - TangentSpace: 1 + BaseColorTexture: TwoMeshTwoMaterial/FBXTestTexture.png Node Name: UVMap -Node Path: RootNode.Cube_optimized.UVMap +Node Path: RootNode.Cube.Cube_1_optimized.UVMap Node Type: MeshVertexUVData UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UVMap -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cube_optimized.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Cube.Cube_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 24. Hash: 13438447437797057049 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cube_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cube.Cube_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 24. Hash: 11372562338897179017 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Cube_optimized.transform +Node Path: RootNode.Cube.Cube_1_optimized.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -178,7 +192,7 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: SingleMaterial -Node Path: RootNode.Cube_optimized.SingleMaterial +Node Path: RootNode.Cube.Cube_1_optimized.SingleMaterial Node Type: MaterialData MaterialName: SingleMaterial UniqueId: 14432700632681398127 @@ -207,27 +221,130 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: TwoMeshTwoMaterial/FBXTestTexture.png +Node Name: transform +Node Path: RootNode.Cylinder.Cylinder_1.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: <-4.388482, 0.000000, 0.000000> + Node Name: UVMap -Node Path: RootNode.Cylinder_optimized.UVMap +Node Path: RootNode.Cylinder.Cylinder_1.UVMap +Node Type: MeshVertexUVData + UVs: Count 192. Hash: 27253578623892681 + UVCustomName: UVMap + +Node Name: SecondMaterial +Node Path: RootNode.Cylinder.Cylinder_1.SecondMaterial +Node Type: MaterialData + MaterialName: SecondMaterial + UniqueId: 5229255358802505087 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.800000, 0.800000, 0.800000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 25.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: TwoMeshTwoMaterial/FBXSecondTestTexture.png + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: TwoMeshTwoMaterial/FBXSecondTestTexture.png + +Node Name: TangentSet_0 +Node Path: RootNode.Cylinder.Cylinder_1.TangentSet_0 +Node Type: MeshVertexTangentData + Tangents: Count 192. Hash: 11165448242141781141 + GenerationMethod: 1 + SetIndex: 0 + +Node Name: BitangentSet_0 +Node Path: RootNode.Cylinder.Cylinder_1.BitangentSet_0 +Node Type: MeshVertexBitangentData + Bitangents: Count 192. Hash: 7987814487334449536 + GenerationMethod: 1 + +Node Name: transform +Node Path: RootNode.Cylinder.Cylinder_2.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: <-4.388482, 0.000000, 0.000000> + +Node Name: UVMap +Node Path: RootNode.Cylinder.Cylinder_2.UVMap +Node Type: MeshVertexUVData + UVs: Count 192. Hash: 27253578623892681 + UVCustomName: UVMap + +Node Name: SecondMaterial +Node Path: RootNode.Cylinder.Cylinder_2.SecondMaterial +Node Type: MaterialData + MaterialName: SecondMaterial + UniqueId: 5229255358802505087 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.800000, 0.800000, 0.800000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 25.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: TwoMeshTwoMaterial/FBXSecondTestTexture.png + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: TwoMeshTwoMaterial/FBXSecondTestTexture.png + +Node Name: UVMap +Node Path: RootNode.Cylinder.Cylinder_1_optimized.UVMap Node Type: MeshVertexUVData UVs: Count 192. Hash: 13790301632763350589 UVCustomName: UVMap -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cylinder_optimized.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Cylinder.Cylinder_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 192. Hash: 7293001660047850407 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cylinder_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cylinder.Cylinder_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 192. Hash: 2874689498270494796 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Cylinder_optimized.transform +Node Path: RootNode.Cylinder.Cylinder_1_optimized.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -236,7 +353,7 @@ Node Type: TransformData Transl: <-4.388482, 0.000000, 0.000000> Node Name: SecondMaterial -Node Path: RootNode.Cylinder_optimized.SecondMaterial +Node Path: RootNode.Cylinder.Cylinder_1_optimized.SecondMaterial Node Type: MaterialData MaterialName: SecondMaterial UniqueId: 5229255358802505087 @@ -264,4 +381,3 @@ Node Type: MaterialData AmbientOcclusionTexture: EmissiveTexture: BaseColorTexture: TwoMeshTwoMaterial/FBXSecondTestTexture.png - diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshTwoMaterial/multiple_mesh_multiple_material_override.dbgsg b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshTwoMaterial/multiple_mesh_multiple_material_override.dbgsg index 6c6b6d552d..8426a68d3b 100644 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshTwoMaterial/multiple_mesh_multiple_material_override.dbgsg +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoMeshTwoMaterial/multiple_mesh_multiple_material_override.dbgsg @@ -1,32 +1,59 @@ ProductName: multiple_mesh_multiple_material.dbgsg debugSceneGraphVersion: 1 multiple_mesh_multiple_material -Node Name: Cube -Node Path: RootNode.Cube +Node Name: RootNode +Node Path: RootNode +Node Type: RootBoneData + WorldTransform: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: Cube_1 +Node Path: RootNode.Cube.Cube_1 Node Type: MeshData Positions: Count 24. Hash: 8661923109306356285 Normals: Count 24. Hash: 5807525742165000561 FaceList: Count 12. Hash: 9888799799190757436 FaceMaterialIds: Count 12. Hash: 7110546404675862471 -Node Name: Cylinder -Node Path: RootNode.Cylinder -Node Type: MeshData - Positions: Count 192. Hash: 1283526254311745349 - Normals: Count 192. Hash: 1873340970602844856 - FaceList: Count 124. Hash: 3728991722746136013 - FaceMaterialIds: Count 124. Hash: 2372486708814455910 +Node Name: Cube_2 +Node Path: RootNode.Cube.Cube_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> -Node Name: Cube_optimized -Node Path: RootNode.Cube_optimized +Node Name: Cube_1_optimized +Node Path: RootNode.Cube.Cube_1_optimized Node Type: MeshData Positions: Count 24. Hash: 8661923109306356285 Normals: Count 24. Hash: 5807525742165000561 FaceList: Count 12. Hash: 9888799799190757436 FaceMaterialIds: Count 12. Hash: 7110546404675862471 +Node Name: Cylinder_1 +Node Path: RootNode.Cylinder.Cylinder_1 +Node Type: MeshData + Positions: Count 192. Hash: 1283526254311745349 + Normals: Count 192. Hash: 1873340970602844856 + FaceList: Count 124. Hash: 3728991722746136013 + FaceMaterialIds: Count 124. Hash: 2372486708814455910 + +Node Name: Cylinder_2 +Node Path: RootNode.Cylinder.Cylinder_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: <-4.388482, 0.000000, 0.000000> + Node Name: transform -Node Path: RootNode.Cube.transform +Node Path: RootNode.Cube.Cube_1.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -35,13 +62,13 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.Cube.UVMap +Node Path: RootNode.Cube.Cube_1.UVMap Node Type: MeshVertexUVData UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UVMap Node Name: SingleMaterial -Node Path: RootNode.Cube.SingleMaterial +Node Path: RootNode.Cube.Cube_1.SingleMaterial Node Type: MaterialData MaterialName: SingleMaterial UniqueId: 14432700632681398127 @@ -70,42 +97,42 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: TwoMeshTwoMaterial/FBXTestTexture.png -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cube.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Cube.Cube_1.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 24. Hash: 13438447437797057049 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cube.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cube.Cube_1.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 24. Hash: 11372562338897179017 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Cylinder.transform +Node Path: RootNode.Cube.Cube_2.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> BasisY: < 0.000000, -0.000016, 100.000000> BasisZ: < 0.000000, -100.000000, -0.000016> - Transl: <-4.388482, 0.000000, 0.000000> + Transl: < 0.000000, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.Cylinder.UVMap +Node Path: RootNode.Cube.Cube_2.UVMap Node Type: MeshVertexUVData - UVs: Count 192. Hash: 27253578623892681 + UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UVMap -Node Name: SecondMaterial -Node Path: RootNode.Cylinder.SecondMaterial +Node Name: SingleMaterial +Node Path: RootNode.Cube.Cube_2.SingleMaterial Node Type: MaterialData - MaterialName: SecondMaterial - UniqueId: 5229255358802505087 + MaterialName: SingleMaterial + UniqueId: 14432700632681398127 IsNoDraw: false - DiffuseColor: < 0.800000, 0.800000, 0.800000> - SpecularColor: < 0.800000, 0.800000, 0.800000> + DiffuseColor: < 0.814049, 0.814049, 0.814049> + SpecularColor: < 0.814049, 0.814049, 0.814049> EmissiveColor: < 0.000000, 0.000000, 0.000000> Opacity: 1.000000 Shininess: 25.000000 @@ -118,7 +145,7 @@ Node Type: MaterialData UseEmissiveMap: Not set EmissiveIntensity: Not set UseAOMap: Not set - DiffuseTexture: TwoMeshTwoMaterial/FBXSecondTestTexture.png + DiffuseTexture: TwoMeshTwoMaterial/FBXTestTexture.png SpecularTexture: BumpTexture: NormalTexture: @@ -126,42 +153,29 @@ Node Type: MaterialData RoughnessTexture: AmbientOcclusionTexture: EmissiveTexture: - BaseColorTexture: TwoMeshTwoMaterial/FBXSecondTestTexture.png - -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cylinder.TangentSet_MikkT_0 -Node Type: MeshVertexTangentData - Tangents: Count 192. Hash: 11165448242141781141 - TangentSpace: 1 - SetIndex: 0 - -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cylinder.BitangentSet_MikkT_0 -Node Type: MeshVertexBitangentData - Bitangents: Count 192. Hash: 7987814487334449536 - TangentSpace: 1 + BaseColorTexture: TwoMeshTwoMaterial/FBXTestTexture.png Node Name: UVMap -Node Path: RootNode.Cube_optimized.UVMap +Node Path: RootNode.Cube.Cube_1_optimized.UVMap Node Type: MeshVertexUVData UVs: Count 24. Hash: 1622169145591646736 UVCustomName: UVMap -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cube_optimized.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Cube.Cube_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 24. Hash: 13438447437797057049 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cube_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cube.Cube_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 24. Hash: 11372562338897179017 - TangentSpace: 1 + GenerationMethod: 1 Node Name: transform -Node Path: RootNode.Cube_optimized.transform +Node Path: RootNode.Cube.Cube_1_optimized.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -170,7 +184,7 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: SingleMaterial -Node Path: RootNode.Cube_optimized.SingleMaterial +Node Path: RootNode.Cube.Cube_1_optimized.SingleMaterial Node Type: MaterialData MaterialName: SingleMaterial UniqueId: 14432700632681398127 @@ -199,3 +213,105 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: TwoMeshTwoMaterial/FBXTestTexture.png +Node Name: transform +Node Path: RootNode.Cylinder.Cylinder_1.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: <-4.388482, 0.000000, 0.000000> + +Node Name: UVMap +Node Path: RootNode.Cylinder.Cylinder_1.UVMap +Node Type: MeshVertexUVData + UVs: Count 192. Hash: 27253578623892681 + UVCustomName: UVMap + +Node Name: SecondMaterial +Node Path: RootNode.Cylinder.Cylinder_1.SecondMaterial +Node Type: MaterialData + MaterialName: SecondMaterial + UniqueId: 5229255358802505087 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.800000, 0.800000, 0.800000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 25.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: TwoMeshTwoMaterial/FBXSecondTestTexture.png + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: TwoMeshTwoMaterial/FBXSecondTestTexture.png + +Node Name: TangentSet_0 +Node Path: RootNode.Cylinder.Cylinder_1.TangentSet_0 +Node Type: MeshVertexTangentData + Tangents: Count 192. Hash: 11165448242141781141 + GenerationMethod: 1 + SetIndex: 0 + +Node Name: BitangentSet_0 +Node Path: RootNode.Cylinder.Cylinder_1.BitangentSet_0 +Node Type: MeshVertexBitangentData + Bitangents: Count 192. Hash: 7987814487334449536 + GenerationMethod: 1 + +Node Name: transform +Node Path: RootNode.Cylinder.Cylinder_2.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: <-4.388482, 0.000000, 0.000000> + +Node Name: UVMap +Node Path: RootNode.Cylinder.Cylinder_2.UVMap +Node Type: MeshVertexUVData + UVs: Count 192. Hash: 27253578623892681 + UVCustomName: UVMap + +Node Name: SecondMaterial +Node Path: RootNode.Cylinder.Cylinder_2.SecondMaterial +Node Type: MaterialData + MaterialName: SecondMaterial + UniqueId: 5229255358802505087 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.800000, 0.800000, 0.800000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 25.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: TwoMeshTwoMaterial/FBXSecondTestTexture.png + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: TwoMeshTwoMaterial/FBXSecondTestTexture.png diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/VertexColor/vertexcolor.dbgsg b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/VertexColor/vertexcolor.dbgsg index 990e8ff628..8ccccfca4b 100644 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/VertexColor/vertexcolor.dbgsg +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/VertexColor/vertexcolor.dbgsg @@ -1,30 +1,48 @@ ProductName: vertexcolor.dbgsg debugSceneGraphVersion: 1 vertexcolor -Node Name: Cube -Node Path: RootNode.Cube +Node Name: RootNode +Node Path: RootNode +Node Type: RootBoneData + WorldTransform: + BasisX: < 1.000000, 0.000000, 0.000000> + BasisY: < 0.000000, 1.000000, 0.000000> + BasisZ: < 0.000000, 0.000000, 1.000000> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: Cube_1 +Node Path: RootNode.Cube.Cube_1 Node Type: MeshData Positions: Count 24576. Hash: 7031773714680283213 Normals: Count 24576. Hash: 8968157737282745201 FaceList: Count 12288. Hash: 13183441914179219962 FaceMaterialIds: Count 12288. Hash: 12545154121625736090 -Node Name: Cube_optimized -Node Path: RootNode.Cube_optimized +Node Name: Cube_2 +Node Path: RootNode.Cube.Cube_2 +Node Type: BoneData + WorldTransform: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> + +Node Name: Cube_1_optimized +Node Path: RootNode.Cube.Cube_1_optimized Node Type: MeshData - Positions: Count 24576. Hash: 7031773714680283213 - Normals: Count 24576. Hash: 8968157737282745201 - FaceList: Count 12288. Hash: 13183441914179219962 + Positions: Count 6376. Hash: 10806296444120211070 + Normals: Count 6376. Hash: 3814626075063770280 + FaceList: Count 12288. Hash: 15242182080304859208 FaceMaterialIds: Count 12288. Hash: 12545154121625736090 Node Name: Col0 -Node Path: RootNode.Cube.Col0 +Node Path: RootNode.Cube.Cube_1.Col0 Node Type: MeshVertexColorData Colors: Count 24576. Hash: 17169952715183318502 ColorsCustomName: Node Name: transform -Node Path: RootNode.Cube.transform +Node Path: RootNode.Cube.Cube_1.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -33,13 +51,13 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.Cube.UVMap +Node Path: RootNode.Cube.Cube_1.UVMap Node Type: MeshVertexUVData UVs: Count 24576. Hash: 4554678369329207802 UVCustomName: UVMap Node Name: Material -Node Path: RootNode.Cube.Material +Node Path: RootNode.Cube.Cube_1.Material Node Type: MaterialData MaterialName: Material UniqueId: 11127505492038345244 @@ -68,46 +86,97 @@ Node Type: MaterialData EmissiveTexture: BaseColorTexture: -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cube.TangentSet_MikkT_0 +Node Name: TangentSet_0 +Node Path: RootNode.Cube.Cube_1.TangentSet_0 Node Type: MeshVertexTangentData Tangents: Count 24576. Hash: 13321090379606717973 - TangentSpace: 1 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cube.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cube.Cube_1.BitangentSet_0 Node Type: MeshVertexBitangentData Bitangents: Count 24576. Hash: 17217515414004886507 - TangentSpace: 1 + GenerationMethod: 1 + +Node Name: Col0 +Node Path: RootNode.Cube.Cube_2.Col0 +Node Type: MeshVertexColorData + Colors: Count 24576. Hash: 17169952715183318502 + ColorsCustomName: + +Node Name: transform +Node Path: RootNode.Cube.Cube_2.transform +Node Type: TransformData + Matrix: + BasisX: < 100.000000, 0.000000, 0.000000> + BasisY: < 0.000000, -0.000016, 100.000000> + BasisZ: < 0.000000, -100.000000, -0.000016> + Transl: < 0.000000, 0.000000, 0.000000> Node Name: UVMap -Node Path: RootNode.Cube_optimized.UVMap +Node Path: RootNode.Cube.Cube_2.UVMap Node Type: MeshVertexUVData UVs: Count 24576. Hash: 4554678369329207802 UVCustomName: UVMap -Node Name: TangentSet_MikkT_0 -Node Path: RootNode.Cube_optimized.TangentSet_MikkT_0 +Node Name: Material +Node Path: RootNode.Cube.Cube_2.Material +Node Type: MaterialData + MaterialName: Material + UniqueId: 11127505492038345244 + IsNoDraw: false + DiffuseColor: < 0.800000, 0.800000, 0.800000> + SpecularColor: < 0.800000, 0.800000, 0.800000> + EmissiveColor: < 0.000000, 0.000000, 0.000000> + Opacity: 1.000000 + Shininess: 36.000000 + UseColorMap: Not set + BaseColor: Not set + UseMetallicMap: Not set + MetallicFactor: Not set + UseRoughnessMap: Not set + RoughnessFactor: Not set + UseEmissiveMap: Not set + EmissiveIntensity: Not set + UseAOMap: Not set + DiffuseTexture: + SpecularTexture: + BumpTexture: + NormalTexture: + MetallicTexture: + RoughnessTexture: + AmbientOcclusionTexture: + EmissiveTexture: + BaseColorTexture: + +Node Name: UVMap +Node Path: RootNode.Cube.Cube_1_optimized.UVMap +Node Type: MeshVertexUVData + UVs: Count 6376. Hash: 12957930967905951851 + UVCustomName: UVMap + +Node Name: TangentSet_0 +Node Path: RootNode.Cube.Cube_1_optimized.TangentSet_0 Node Type: MeshVertexTangentData - Tangents: Count 24576. Hash: 13321090379606717973 - TangentSpace: 1 + Tangents: Count 6376. Hash: 7712841033379094373 + GenerationMethod: 1 SetIndex: 0 -Node Name: BitangentSet_MikkT_0 -Node Path: RootNode.Cube_optimized.BitangentSet_MikkT_0 +Node Name: BitangentSet_0 +Node Path: RootNode.Cube.Cube_1_optimized.BitangentSet_0 Node Type: MeshVertexBitangentData - Bitangents: Count 24576. Hash: 17217515414004886507 - TangentSpace: 1 + Bitangents: Count 6376. Hash: 12547048737213169362 + GenerationMethod: 1 Node Name: Col0 -Node Path: RootNode.Cube_optimized.Col0 +Node Path: RootNode.Cube.Cube_1_optimized.Col0 Node Type: MeshVertexColorData - Colors: Count 24576. Hash: 17169952715183318502 + Colors: Count 6376. Hash: 8761962599807935159 ColorsCustomName: Node Name: transform -Node Path: RootNode.Cube_optimized.transform +Node Path: RootNode.Cube.Cube_1_optimized.transform Node Type: TransformData Matrix: BasisX: < 100.000000, 0.000000, 0.000000> @@ -116,7 +185,7 @@ Node Type: TransformData Transl: < 0.000000, 0.000000, 0.000000> Node Name: Material -Node Path: RootNode.Cube_optimized.Material +Node Path: RootNode.Cube.Cube_1_optimized.Material Node Type: MaterialData MaterialName: Material UniqueId: 11127505492038345244 @@ -144,4 +213,3 @@ Node Type: MaterialData AmbientOcclusionTexture: EmissiveTexture: BaseColorTexture: - diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/fbx_tests.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/fbx_tests.py index 5cb61da68e..71f200074c 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/fbx_tests.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/fbx_tests.py @@ -67,7 +67,7 @@ blackbox_fbx_tests = [ builder_guid=b"bd8bf65894854fe3830e8ec3a23c35f3", status=4, error_count=0, - warning_count=0, + warning_count=1, products = [ asset_db_utils.DBProduct( product_name='onemeshonematerial/onemeshonematerial.dbgsg', @@ -99,7 +99,7 @@ blackbox_fbx_tests = [ builder_guid=b"bd8bf65894854fe3830e8ec3a23c35f3", status=4, error_count=0, - warning_count=9, + warning_count=22, products = [ asset_db_utils.DBProduct( product_name='softnaminglod/lodtest.dbgsg', @@ -131,7 +131,7 @@ blackbox_fbx_tests = [ builder_guid=b"bd8bf65894854fe3830e8ec3a23c35f3", status=4, error_count=0, - warning_count=6, + warning_count=14, products = [ asset_db_utils.DBProduct( product_name='softnamingphysics/physicstest.dbgsg', @@ -165,7 +165,7 @@ blackbox_fbx_tests = [ builder_guid=b"bd8bf65894854fe3830e8ec3a23c35f3", status=4, error_count=0, - warning_count=0, + warning_count=2, products = [ asset_db_utils.DBProduct( product_name='twomeshonematerial/multiple_mesh_one_material.dbgsg', @@ -197,7 +197,7 @@ blackbox_fbx_tests = [ builder_guid=b"bd8bf65894854fe3830e8ec3a23c35f3", status=4, error_count=0, - warning_count=0, + warning_count=2, products= [ asset_db_utils.DBProduct( product_name='twomeshlinkedmaterials/multiple_mesh_linked_materials.dbgsg', @@ -230,7 +230,7 @@ blackbox_fbx_tests = [ builder_guid=b"bd8bf65894854fe3830e8ec3a23c35f3", status=4, error_count=0, - warning_count=0, + warning_count=1, products = [ asset_db_utils.DBProduct( product_name='onemeshmultiplematerials/single_mesh_multiple_materials.dbgsg', @@ -260,7 +260,7 @@ blackbox_fbx_tests = [ builder_guid=b"bd8bf65894854fe3830e8ec3a23c35f3", status=4, error_count=0, - warning_count=0, + warning_count=1, products=[ asset_db_utils.DBProduct( product_name='vertexcolor/vertexcolor.dbgsg', @@ -275,6 +275,38 @@ blackbox_fbx_tests = [ id="35796285", marks=pytest.mark.test_case_id("C35796285"), ), + pytest.param( + BlackboxAssetTest( + test_name= "MotionTest_RunAP_SuccessWithMatchingProducts", + asset_folder= "Motion", + scene_debug_file="Jack_Idle_Aim_ZUp.dbgsg", + assets = [ + asset_db_utils.DBSourceAsset( + source_file_name = "Jack_Idle_Aim_ZUp.fbx", + uuid = b"eda904ae0e145f8b973d57fc5809918b", + jobs = [ + asset_db_utils.DBJob( + job_key= "Scene compilation", + builder_guid=b"bd8bf65894854fe3830e8ec3a23c35f3", + status=4, + error_count=0, + warning_count=0, + products = [ + asset_db_utils.DBProduct( + product_name='motion/jack_idle_aim_zup.dbgsg', + sub_id=-517610290, + asset_type=b'07f289d14dc74c4094b40a53bbcb9f0b'), + asset_db_utils.DBProduct( + product_name='motion/jack_idle_aim_zup.motion', + sub_id=186392073, + asset_type=b'00494b8e75784ba28b28272e90680787') + ] + ), + ] + ) + ] + ), + ), ] @@ -296,7 +328,7 @@ blackbox_fbx_special_tests = [ builder_guid=b"bd8bf65894854fe3830e8ec3a23c35f3", status=4, error_count=0, - warning_count=0, + warning_count=2, products = [ asset_db_utils.DBProduct( product_name='twomeshtwomaterial/multiple_mesh_multiple_material.dbgsg', @@ -317,7 +349,7 @@ blackbox_fbx_special_tests = [ builder_guid=b"bd8bf65894854fe3830e8ec3a23c35f3", status=4, error_count=0, - warning_count=0, + warning_count=2, products = [ asset_db_utils.DBProduct( product_name='twomeshtwomaterial/multiple_mesh_multiple_material.dbgsg', @@ -387,7 +419,6 @@ class TestsFBX_AllPlatforms(object): self.run_fbx_test(workspace, ap_setup_fixture, asset_processor, project, blackbox_param, True) - def populateAssetInfo(self, workspace, project, assets): # Check that each given source asset resulted in the expected jobs and products. @@ -398,7 +429,6 @@ class TestsFBX_AllPlatforms(object): product.product_name = job.platform + "/" \ + product.product_name - def run_fbx_test(self, workspace, ap_setup_fixture, asset_processor, project, blackbox_params: BlackboxAssetTest, overrideAsset = False): """ diff --git a/AutomatedTesting/Gem/PythonTests/editor_test_testing/CMakeLists.txt b/AutomatedTesting/Gem/PythonTests/editor_test_testing/CMakeLists.txt new file mode 100644 index 0000000000..dbf26d66cd --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/editor_test_testing/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 timeouts on jenkins, investigation is needed. Commment for now +# +#if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS) +# ly_add_pytest( +# NAME AutomatedTesting::EditorTestTesting +# TEST_SUITE main +# TEST_SERIAL +# PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main.py +# RUNTIME_DEPENDENCIES +# Legacy::Editor +# AZ::AssetProcessor +# AutomatedTesting.Assets +# COMPONENT +# TestTools +# ) +#endif() diff --git a/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py b/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py index 7c7e063951..15ba6690a1 100644 --- a/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py +++ b/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py @@ -35,10 +35,11 @@ class TestEditorTest: @classmethod def setup_class(cls): TestEditorTest.args = sys.argv.copy() - build_dir_arg_index = TestEditorTest.args.index("--build-directory") - if build_dir_arg_index < 0: - print("Error: Must pass --build-directory argument in order to run this test") - sys.exit(-2) + build_dir_arg_index = -1 + try: + build_dir_arg_index = TestEditorTest.args.index("--build-directory") + except ValueError as ex: + raise ValueError("Must pass --build-directory argument in order to run this test") TestEditorTest.args[build_dir_arg_index+1] = os.path.abspath(TestEditorTest.args[build_dir_arg_index+1]) TestEditorTest.args.append("-s") diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/TestSuite_Main_Optimized.py b/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/TestSuite_Main_Optimized.py index 0461ff2647..1c3652cae9 100644 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/TestSuite_Main_Optimized.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/TestSuite_Main_Optimized.py @@ -9,6 +9,7 @@ import os import pytest import ly_test_tools.environment.file_system as file_system +import ly_test_tools._internal.pytest_plugin as internal_plugin from ly_test_tools.o3de.editor_test import EditorSingleTest, EditorSharedTest, EditorParallelTest, EditorTestSuite @@ -79,6 +80,8 @@ class TestAutomation(EditorTestSuite): class test_LandscapeCanvas_GradientNodes_EntityRemovedOnNodeDelete(EditorSharedTest): from .EditorScripts import GradientNodes_EntityRemovedOnNodeDelete as test_module + @pytest.mark.skipif("debug" == os.path.basename(internal_plugin.build_directory), + reason="https://github.com/o3de/o3de/issues/4872") class test_LandscapeCanvas_GraphUpdates_UpdateComponents(EditorSharedTest): from .EditorScripts import GraphUpdates_UpdateComponents as test_module diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab.py b/AutomatedTesting/Gem/PythonTests/prefab/Prefab.py deleted file mode 100644 index 9b4d2d1393..0000000000 --- a/AutomatedTesting/Gem/PythonTests/prefab/Prefab.py +++ /dev/null @@ -1,226 +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 -""" - -from __future__ import annotations -from collections import Counter -from collections import deque -from os import path - -from PySide2 import QtWidgets - -from azlmbr.entity import EntityId -from azlmbr.math import Vector3 -from editor_python_test_tools.editor_entity_utils import EditorEntity -from editor_python_test_tools.utils import Report - -import azlmbr.bus as bus -import azlmbr.prefab as prefab -import editor_python_test_tools.pyside_utils as pyside_utils -import prefab.Prefab_Test_Utils as prefab_test_utils - -# This is a helper class which contains some of the useful information about a prefab instance. -class PrefabInstance: - - def __init__(self, prefab_file_name: str=None, container_entity: EditorEntity=EntityId()): - self.prefab_file_name: str = prefab_file_name - self.container_entity: EditorEntity = container_entity - - def __eq__(self, other): - return other and self.container_entity.id == other.container_entity.id - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return hash(self.container_entity.id) - - """ - See if this instance is valid to be used with other prefab operations. - :return: Whether the target instance is valid or not. - """ - def is_valid() -> bool: - return self.container_entity.id.IsValid() and self.prefab_file_name in Prefab.existing_prefabs - - """ - Reparent this instance to target parent entity. - The function will also check pop up dialog ui in editor to see if there's prefab cyclical dependency error while reparenting prefabs. - :param parent_entity_id: The id of the entity this instance should be a child of in the transform hierarchy next. - """ - async def ui_reparent_prefab_instance(self, parent_entity_id: EntityId): - container_entity_id_before_reparent = self.container_entity.id - - original_parent = EditorEntity(self.container_entity.get_parent_id()) - original_parent_before_reparent_children_ids = set(original_parent.get_children_ids()) - - new_parent = EditorEntity(parent_entity_id) - new_parent_before_reparent_children_ids = set(new_parent.get_children_ids()) - - pyside_utils.run_soon(lambda: self.container_entity.set_parent_entity(parent_entity_id)) - pyside_utils.run_soon(lambda: prefab_test_utils.wait_for_propagation()) - - try: - active_modal_widget = await pyside_utils.wait_for_modal_widget() - error_message_box = active_modal_widget.findChild(QtWidgets.QMessageBox) - ok_button = error_message_box.button(QtWidgets.QMessageBox.Ok) - ok_button.click() - assert False, "Cyclical dependency detected while reparenting prefab" - except pyside_utils.EventLoopTimeoutException: - pass - - original_parent_after_reparent_children_ids = set(original_parent.get_children_ids()) - assert len(original_parent_after_reparent_children_ids) == len(original_parent_before_reparent_children_ids) - 1, \ - "The children count of the Prefab Instance's original parent should be decreased by 1." - assert not container_entity_id_before_reparent in original_parent_after_reparent_children_ids, \ - "This Prefab Instance is still a child entity of its original parent entity." - - new_parent_after_reparent_children_ids = set(new_parent.get_children_ids()) - assert len(new_parent_after_reparent_children_ids) == len(new_parent_before_reparent_children_ids) + 1, \ - "The children count of the Prefab Instance's new parent should be increased by 1." - - container_entity_id_after_reparent = set(new_parent_after_reparent_children_ids).difference(new_parent_before_reparent_children_ids).pop() - reparented_container_entity = EditorEntity(container_entity_id_after_reparent) - reparented_container_entity_parent_id = reparented_container_entity.get_parent_id() - has_correct_parent = reparented_container_entity_parent_id.ToString() == parent_entity_id.ToString() - assert has_correct_parent, "Prefab Instance reparented is *not* under the expected parent entity" - - self.container_entity = reparented_container_entity - -# This is a helper class which contains some of the useful information about a prefab template. -class Prefab: - - existing_prefabs = {} - - def __init__(self, file_name: str): - self.file_name:str = file_name - self.file_path: str = prefab_test_utils.get_prefab_file_path(file_name) - self.instances: set[PrefabInstance] = set() - - """ - Check if a prefab is ready to be used to generate its instances. - :param file_name: A unique file name of the target prefab. - :return: Whether the target prefab is loaded or not. - """ - @classmethod - def is_prefab_loaded(cls, file_name: str) -> bool: - return file_name in Prefab.existing_prefabs - - """ - Check if a prefab exists in the directory for files of prefab tests. - :param file_name: A unique file name of the target prefab. - :return: Whether the target prefab exists or not. - """ - @classmethod - def prefab_exists(cls, file_name: str) -> bool: - file_path = prefab_test_utils.get_prefab_file_path(file_name) - return path.exists(file_path) - - """ - Return a prefab which can be used immediately. - :param file_name: A unique file name of the target prefab. - :return: The prefab with given file name. - """ - @classmethod - def get_prefab(cls, file_name: str) -> Prefab: - if Prefab.is_prefab_loaded(file_name): - return Prefab.existing_prefabs[file_name] - else: - assert Prefab.prefab_exists(file_name), f"Attempted to get a prefab {file_name} that doesn't exist" - new_prefab = Prefab(file_name) - Prefab.existing_prefabs[file_name] = Prefab(file_name) - return new_prefab - - """ - Create a prefab in memory and return it. The very first instance of this prefab will also be created. - :param entities: The entities that should form the new prefab (along with their descendants). - :param file_name: A unique file name of new prefab. - :param prefab_instance_name: A name for the very first instance generated while prefab creation. The default instance name is the same as file_name. - :return: Created Prefab object and the very first PrefabInstance object owned by the prefab. - """ - @classmethod - def create_prefab(cls, entities: list[EditorEntity], file_name: str, prefab_instance_name: str=None) -> (Prefab, PrefabInstance): - assert not Prefab.is_prefab_loaded(file_name), f"Can't create Prefab '{file_name}' since the prefab already exists" - - new_prefab = Prefab(file_name) - entity_ids = [entity.id for entity in entities] - create_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'CreatePrefabInMemory', entity_ids, new_prefab.file_path) - assert create_prefab_result.IsSuccess(), f"Prefab operation 'CreatePrefab' failed. Error: {create_prefab_result.GetError()}" - - container_entity_id = create_prefab_result.GetValue() - container_entity = EditorEntity(container_entity_id) - - if prefab_instance_name: - container_entity.set_name(prefab_instance_name) - - prefab_test_utils.wait_for_propagation() - - new_prefab_instance = PrefabInstance(file_name, EditorEntity(container_entity_id)) - new_prefab.instances.add(new_prefab_instance) - Prefab.existing_prefabs[file_name] = new_prefab - return new_prefab, new_prefab_instance - - """ - Remove target prefab instances. - :param prefab_instances: Instances to be removed. - """ - @classmethod - def remove_prefabs(cls, prefab_instances: list[PrefabInstance]): - entity_ids_to_remove = [] - entity_id_queue = [prefab_instance.container_entity for prefab_instance in prefab_instances] - while entity_id_queue: - entity = entity_id_queue.pop(0) - children_entity_ids = entity.get_children_ids() - for child_entity_id in children_entity_ids: - entity_id_queue.append(EditorEntity(child_entity_id)) - - entity_ids_to_remove.append(entity.id) - - container_entity_ids = [prefab_instance.container_entity.id for prefab_instance in prefab_instances] - delete_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DeleteEntitiesAndAllDescendantsInInstance', container_entity_ids) - assert delete_prefab_result.IsSuccess(), f"Prefab operation 'DeleteEntitiesAndAllDescendantsInInstance' failed. Error: {delete_prefab_result.GetError()}" - - prefab_test_utils.wait_for_propagation() - - entity_ids_after_delete = set(prefab_test_utils.get_all_entities()) - for entity_id_removed in entity_ids_to_remove: - if entity_id_removed in entity_ids_after_delete: - assert prefab_entities_deleted, "Not all entities and descendants in target prefabs are deleted." - - for instance in prefab_instances: - instance_deleted_prefab = Prefab.get_prefab(instance.prefab_file_name) - instance_deleted_prefab.instances.remove(instance) - instance = PrefabInstance() - - """ - Instantiate an instance of this prefab. - :param parent_entity: The entity the prefab should be a child of in the transform hierarchy. - :param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name. - :param prefab_position: The position in world space the prefab should be instantiated in. - :return: Instantiated PrefabInstance object owned by this prefab. - """ - def instantiate(self, parent_entity: EditorEntity=None, name: str=None, prefab_position: Vector3=Vector3()) -> PrefabInstance: - parent_entity_id = parent_entity.id if parent_entity is not None else EntityId() - - instantiate_prefab_result = prefab.PrefabPublicRequestBus( - bus.Broadcast, 'InstantiatePrefab', self.file_path, parent_entity_id, prefab_position) - - assert instantiate_prefab_result.IsSuccess(), f"Prefab operation 'InstantiatePrefab' failed. Error: {instantiate_prefab_result.GetError()}" - - container_entity_id = instantiate_prefab_result.GetValue() - container_entity = EditorEntity(container_entity_id) - - if name: - container_entity.set_name(name) - - prefab_test_utils.wait_for_propagation() - - new_prefab_instance = PrefabInstance(self.file_name, EditorEntity(container_entity_id)) - assert not new_prefab_instance in self.instances, "This prefab instance is already existed before this instantiation." - self.instances.add(new_prefab_instance) - - prefab_test_utils.check_entity_at_position(container_entity_id, prefab_position) - - return new_prefab_instance diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_Test_Utils.py b/AutomatedTesting/Gem/PythonTests/prefab/Prefab_Test_Utils.py deleted file mode 100644 index 3e19911449..0000000000 --- a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_Test_Utils.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 - -from azlmbr.entity import EntityId -from azlmbr.math import Vector3 -from editor_python_test_tools.editor_entity_utils import EditorEntity -from editor_python_test_tools.utils import Report -from editor_python_test_tools.utils import TestHelper as helper - -import azlmbr.bus as bus -import azlmbr.components as components -import azlmbr.entity as entity -import azlmbr.legacy.general as general - -def get_prefab_file_name(prefab_name): - return prefab_name + ".prefab" - -def get_prefab_file_path(prefab_name): - return os.path.join(os.path.dirname(os.path.abspath(__file__)), get_prefab_file_name(prefab_name)) - -def find_entities_by_name(entity_name): - searchFilter = entity.SearchFilter() - searchFilter.names = [entity_name] - return entity.SearchBus(bus.Broadcast, 'SearchEntities', searchFilter) - -def get_all_entities(): - return entity.SearchBus(bus.Broadcast, 'SearchEntities', entity.SearchFilter()) - -def check_entity_at_position(entity_id, expected_entity_position): - entity_at_expected_position_result = ( - "entity is at expected position", - "entity is *not* at expected position") - - actual_entity_position = components.TransformBus(bus.Event, "GetWorldTranslation", entity_id) - is_at_position = actual_entity_position.IsClose(expected_entity_position) - Report.result(entity_at_expected_position_result, is_at_position) - - if not is_at_position: - Report.info(f"Entity '{entity_id.ToString()}'\'s expected position: {expected_entity_position.ToString()}, actual position: {actual_entity_position.ToString()}") - - return is_at_position - -def check_entity_children_count(entity_id, expected_children_count): - entity_children_count_matched_result = ( - "Entity with a unique name found", - "Entity with a unique name *not* found") - - entity = EditorEntity(entity_id) - children_entity_ids = entity.get_children_ids() - entity_children_count_matched = len(children_entity_ids) == expected_children_count - Report.result(entity_children_count_matched_result, entity_children_count_matched) - - if not entity_children_count_matched: - Report.info(f"Entity '{entity_id.ToString()}' actual children count: {len(children_entity_ids)}. Expected children count: {expected_children_count}") - - return entity_children_count_matched - -def get_children_ids_by_name(entity_id, entity_name): - entity = EditorEntity(entity_id) - children_entity_ids = entity.get_children_ids() - - result = [] - for child_entity_id in children_entity_ids: - child_entity = EditorEntity(child_entity_id) - child_entity_name = child_entity.get_name() - if child_entity_name == entity_name: - result.append(child_entity_id) - - return result - -def wait_for_propagation(): - general.idle_wait_frames(1) - -def open_base_tests_level(): - helper.init_idle() - helper.open_level("Prefab", "Base") diff --git a/AutomatedTesting/Levels/Base/Base.ly b/AutomatedTesting/Levels/Base/Base.ly index 8ce19923c0..0d0aa64648 100644 --- a/AutomatedTesting/Levels/Base/Base.ly +++ b/AutomatedTesting/Levels/Base/Base.ly @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4937547ca4c486ef59656314401933217e0e0401fec103e1fb91c25ec60a177 -size 2806 +oid sha256:a5f9e27e0f22c31ca61d866fb594c6fde5b8ceb891e17dda075fa1e0033ec2b9 +size 1666 diff --git a/Code/Editor/AboutDialog.cpp b/Code/Editor/AboutDialog.cpp index 2c76526731..c0fd39e1ae 100644 --- a/Code/Editor/AboutDialog.cpp +++ b/Code/Editor/AboutDialog.cpp @@ -6,10 +6,7 @@ * */ - - #include "EditorDefs.h" - #include "AboutDialog.h" // Qt @@ -47,14 +44,17 @@ CAboutDialog::CAboutDialog(QString versionText, QString richTextCopyrightNotice, CAboutDialog > QLabel#link { text-decoration: underline; color: #94D2FF; }"); // Prepare background image - m_backgroundImage = AzQtComponents::ScalePixmapForScreenDpi( - QPixmap(QStringLiteral(":/StartupLogoDialog/splashscreen_background_developer_preview.jpg")), - screen(), - QSize(m_enforcedWidth, m_enforcedHeight), + QPixmap image = AzQtComponents::ScalePixmapForScreenDpi( + QPixmap(QStringLiteral(":/StartupLogoDialog/splashscreen_background_2021_11.jpg")), + screen(), QSize(m_imageWidth, m_imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation ); + // Crop image to cut out transparent border + QRect cropRect((m_imageWidth - m_enforcedWidth) / 2, (m_imageHeight - m_enforcedHeight) / 2, m_enforcedWidth, m_enforcedHeight); + m_backgroundImage = AzQtComponents::CropPixmapForScreenDpi(image, screen(), cropRect); + // Draw the Open 3D Engine logo from svg m_ui->m_logo->load(QStringLiteral(":/StartupLogoDialog/o3de_logo.svg")); diff --git a/Code/Editor/AboutDialog.h b/Code/Editor/AboutDialog.h index 229675901e..db079a6ec7 100644 --- a/Code/Editor/AboutDialog.h +++ b/Code/Editor/AboutDialog.h @@ -38,7 +38,9 @@ private: QScopedPointer m_ui; QPixmap m_backgroundImage; - int m_enforcedWidth = 600; - int m_enforcedHeight = 400; + const int m_imageWidth = 668; + const int m_imageHeight = 368; + const int m_enforcedWidth = 600; + const int m_enforcedHeight = 300; }; diff --git a/Code/Editor/AboutDialog.ui b/Code/Editor/AboutDialog.ui index 9341a00d48..a6c5bb5d52 100644 --- a/Code/Editor/AboutDialog.ui +++ b/Code/Editor/AboutDialog.ui @@ -7,7 +7,7 @@ 0 0 600 - 360 + 300 @@ -19,13 +19,13 @@ 600 - 360 + 300 - 600 - 360 + 608 + 300 @@ -69,7 +69,7 @@ 11 - 12 + 10 12 @@ -125,7 +125,7 @@ - Developer Preview + General Availability Qt::AutoText diff --git a/Code/Editor/Controls/ColorGradientCtrl.cpp b/Code/Editor/Controls/ColorGradientCtrl.cpp deleted file mode 100644 index 446e5810c5..0000000000 --- a/Code/Editor/Controls/ColorGradientCtrl.cpp +++ /dev/null @@ -1,923 +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 "EditorDefs.h" - -#include "ColorGradientCtrl.h" - -// Qt -#include -#include - -// AzQtComponents -#include - - -#define MIN_TIME_EPSILON 0.01f - -////////////////////////////////////////////////////////////////////////// -CColorGradientCtrl::CColorGradientCtrl(QWidget* parent) - : QWidget(parent) -{ - m_nActiveKey = -1; - m_nHitKeyIndex = -1; - m_nKeyDrawRadius = 3; - m_bTracking = false; - m_pSpline = nullptr; - m_fMinTime = -1; - m_fMaxTime = 1; - m_fMinValue = -1; - m_fMaxValue = 1; - m_fTooltipScaleX = 1; - m_fTooltipScaleY = 1; - m_bNoTimeMarker = true; - m_bLockFirstLastKey = false; - m_bNoZoom = true; - - ClearSelection(); - - m_bSelectedKeys.reserve(0); - - m_fTimeMarker = -10; - - m_grid.zoom.x = 100; - - setMouseTracking(true); -} - -CColorGradientCtrl::~CColorGradientCtrl() -{ -} - - -///////////////////////////////////////////////////////////////////////////// -// QColorGradientCtrl message handlers - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::resizeEvent(QResizeEvent* event) -{ - QWidget::resizeEvent(event); - - QRect rc(QPoint(0, 0), event->size()); - m_rcGradient = rc; - m_rcGradient.setHeight(m_rcGradient.height() - 11); - //m_rcGradient.DeflateRect(4,4); - - m_grid.rect = m_rcGradient; - if (m_bNoZoom) - { - m_grid.zoom.x = static_cast(m_grid.rect.width()); - } - - m_rcKeys = rc; - m_rcKeys.setTop(m_rcKeys.bottom() - 10); -} - - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetZoom(float fZoom) -{ - m_grid.zoom.x = fZoom; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetOrigin(float fOffset) -{ - m_grid.origin.x = fOffset; -} - -////////////////////////////////////////////////////////////////////////// -QPoint CColorGradientCtrl::KeyToPoint(int nKey) -{ - if (nKey >= 0) - { - return TimeToPoint(m_pSpline->GetKeyTime(nKey)); - } - return QPoint(0, 0); -} - -////////////////////////////////////////////////////////////////////////// -QPoint CColorGradientCtrl::TimeToPoint(float time) -{ - return QPoint(m_grid.WorldToClient(Vec2(time, 0)).x(), m_rcGradient.height() / 2); -} - -////////////////////////////////////////////////////////////////////////// -AZ::Color CColorGradientCtrl::TimeToColor(float time) -{ - ISplineInterpolator::ValueType val; - m_pSpline->Interpolate(time, val); - const AZ::Color col = ValueToColor(val); - return col; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::PointToTimeValue(QPoint point, float& time, ISplineInterpolator::ValueType& val) -{ - time = XOfsToTime(point.x()); - ColorToValue(TimeToColor(time), val); -} - -////////////////////////////////////////////////////////////////////////// -float CColorGradientCtrl::XOfsToTime(int x) -{ - return m_grid.ClientToWorld(QPoint(x, 0)).x; -} - -////////////////////////////////////////////////////////////////////////// -QPoint CColorGradientCtrl::XOfsToPoint(int x) -{ - return TimeToPoint(XOfsToTime(x)); -} - -////////////////////////////////////////////////////////////////////////// -AZ::Color CColorGradientCtrl::XOfsToColor(int x) -{ - return TimeToColor(XOfsToTime(x)); -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::paintEvent(QPaintEvent* e) -{ - QPainter painter(this); - - QRect rcClient = rect(); - - if (m_pSpline) - { - m_bSelectedKeys.resize(m_pSpline->GetKeyCount()); - } - { - if (!isEnabled()) - { - painter.setBrush(palette().button()); - painter.drawRect(rcClient); - return; - } - - ////////////////////////////////////////////////////////////////////////// - // Fill keys backgound. - ////////////////////////////////////////////////////////////////////////// - QRect rcKeys = m_rcKeys.intersected(e->rect()); - painter.setBrush(palette().button()); - painter.drawRect(rcKeys); - ////////////////////////////////////////////////////////////////////////// - - //Draw Keys and Curve - if (m_pSpline) - { - DrawGradient(e, &painter); - DrawKeys(e, &painter); - } - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::DrawGradient(QPaintEvent* e, QPainter* painter) -{ - //Draw Curve - // create and select a thick, white pen - painter->setPen(QPen(QColor(128, 255, 128), 1, Qt::SolidLine)); - - const QRect rcClip = e->rect().intersected(m_rcGradient); - const int right = rcClip.left() + rcClip.width(); - for (int x = rcClip.left(); x < right; x++) - { - const AZ::Color col = XOfsToColor(x); - QPen pen(QColor(col.GetR8(), col.GetG8(), col.GetR8(), col.GetA8()), 1, Qt::SolidLine); - painter->setPen(pen); - painter->drawLine(x, m_rcGradient.top(), x, m_rcGradient.top() + m_rcGradient.height()); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::DrawKeys(QPaintEvent* e, QPainter* painter) -{ - if (!m_pSpline) - { - return; - } - - // create and select a white pen - painter->setPen(QPen(QColor(0, 0, 0), 1, Qt::SolidLine)); - - QRect rcClip = e->rect(); - - m_bSelectedKeys.resize(m_pSpline->GetKeyCount()); - - for (int i = 0; i < m_pSpline->GetKeyCount(); i++) - { - float time = m_pSpline->GetKeyTime(i); - QPoint pt = TimeToPoint(time); - - if (pt.x() < rcClip.left() - 8 || pt.x() > rcClip.left() + rcClip.width() + 8) - { - continue; - } - - const AZ::Color clr = TimeToColor(time); - QBrush brush(QColor(clr.GetR8(), clr.GetG8(), clr.GetB8(), clr.GetA8())); - painter->setBrush(brush); - - // Find the midpoints of the top, right, left, and bottom - // of the client area. They will be the vertices of our polygon. - QPoint pts[3]; - pts[0].rx() = pt.x(); - pts[0].ry() = m_rcKeys.top() + 1; - pts[1].rx() = pt.x() - 5; - pts[1].ry() = m_rcKeys.top() + 8; - pts[2].rx() = pt.x() + 5; - pts[2].ry() = m_rcKeys.top() + 8; - painter->drawPolygon(pts, 3); - - if (m_bSelectedKeys[i]) - { - QPen pen(QColor(200, 0, 0), 1, Qt::SolidLine); - QPen oldPen = painter->pen(); - painter->setPen(pen); - painter->drawPolygon(pts, 3); - painter->setPen(oldPen); - } - } - - if (!m_bNoTimeMarker) - { - QPen timePen(QColor(255, 0, 255), 1, Qt::SolidLine); - painter->setPen(timePen); - QPoint pt = TimeToPoint(m_fTimeMarker); - painter->drawLine(pt.x(), m_rcGradient.top() + 1, pt.x(), m_rcGradient.bottom() - 1); - } -} - -void CColorGradientCtrl::UpdateTooltip(QPoint pos) -{ - if (m_nHitKeyIndex >= 0) - { - float time = m_pSpline->GetKeyTime(m_nHitKeyIndex); - ISplineInterpolator::ValueType val; - m_pSpline->GetKeyValue(m_nHitKeyIndex, val); - - AZ::Color col = TimeToColor(time); - int cont_s = (m_pSpline->GetKeyFlags(m_nHitKeyIndex) >> SPLINE_KEY_TANGENT_IN_SHIFT) & SPLINE_KEY_TANGENT_LINEAR ? 1 : 2; - int cont_d = (m_pSpline->GetKeyFlags(m_nHitKeyIndex) >> SPLINE_KEY_TANGENT_OUT_SHIFT) & SPLINE_KEY_TANGENT_LINEAR ? 1 : 2; - - QString tipText(tr("%1 : %2,%3,%4 [%5,%6]").arg(time * m_fTooltipScaleX, 0, 'f', 2).arg(col.GetR8()).arg(col.GetG8()).arg(col.GetB8()).arg(cont_s).arg(cont_d)); - const QPoint globalPos = mapToGlobal(pos); - QToolTip::showText(mapToGlobal(pos), tipText, this, QRect(globalPos, QSize(1, 1))); - } -} - -///////////////////////////////////////////////////////////////////////////// -//Mouse Message Handlers -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::mousePressEvent(QMouseEvent* event) -{ - if (event->button() == Qt::LeftButton) - { - OnLButtonDown(event); - } - else if (event->button() == Qt::RightButton) - { - OnRButtonDown(event); - } -} - -void CColorGradientCtrl::OnLButtonDown([[maybe_unused]] QMouseEvent* event) -{ - if (m_bTracking) - { - return; - } - if (!m_pSpline) - { - return; - } - - setFocus(); - - switch (m_hitCode) - { - case HIT_KEY: - StartTracking(); - SetActiveKey(m_nHitKeyIndex); - break; - - /* - case HIT_SPLINE: - { - // Cycle the spline slope of the nearest key. - int flags = m_pSpline->GetKeyFlags(m_nHitKeyIndex); - if (m_nHitKeyDist < 0) - // Toggle left side. - flags ^= SPLINE_KEY_TANGENT_LINEAR << SPLINE_KEY_TANGENT_IN_SHIFT; - if (m_nHitKeyDist > 0) - // Toggle right side. - flags ^= SPLINE_KEY_TANGENT_LINEAR << SPLINE_KEY_TANGENT_OUT_SHIFT; - m_pSpline->SetKeyFlags(m_nHitKeyIndex, flags); - m_pSpline->Update(); - - SetActiveKey(-1); - SendNotifyEvent( CLRGRDN_CHANGE ); - if (m_updateCallback) - m_updateCallback(this); - break; - } - */ - - case HIT_NOTHING: - SetActiveKey(-1); - break; - } - update(); -} - - - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::OnRButtonDown([[maybe_unused]] QMouseEvent* event) -{ -} - - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::mouseDoubleClickEvent(QMouseEvent* event) -{ - if (!m_pSpline) - { - return; - } - - if (event->button() != Qt::LeftButton) - { - return; - } - - switch (m_hitCode) - { - case HIT_SPLINE: - { - int iIndex = InsertKey(event->pos()); - SetActiveKey(iIndex); - EditKey(iIndex); - - update(); - } - break; - case HIT_KEY: - { - EditKey(m_nHitKeyIndex); - } - break; - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::mouseMoveEvent(QMouseEvent* event) -{ - if (!m_pSpline) - { - return; - } - - if (!m_bTracking) - { - switch (HitTest(event->pos())) - { - case HIT_SPLINE: - { - setCursor(CMFCUtils::LoadCursor(IDC_ARRWHITE)); - } break; - case HIT_KEY: - { - setCursor(CMFCUtils::LoadCursor(IDC_ARRBLCK)); - } break; - default: - break; - } - } - - if (m_bTracking) - { - TrackKey(event->pos()); - } - - if (m_bTracking || m_nHitKeyIndex >= 0) - { - UpdateTooltip(event->pos()); - } - else - { - QToolTip::hideText(); - } -} - -void CColorGradientCtrl::mouseReleaseEvent(QMouseEvent* event) -{ - if (event->button() == Qt::LeftButton) - { - OnLButtonUp(event); - } - else if (event->button() == Qt::RightButton) - { - OnRButtonUp(event); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::OnLButtonUp(QMouseEvent* event) -{ - if (!m_pSpline) - { - return; - } - - if (m_bTracking) - { - StopTracking(event->pos()); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::OnRButtonUp([[maybe_unused]] QMouseEvent* event) -{ - if (!m_pSpline) - { - return; - } -} - -///////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetActiveKey(int nIndex) -{ - ClearSelection(); - - //Activate New Key - if (nIndex >= 0) - { - m_bSelectedKeys[nIndex] = true; - } - m_nActiveKey = nIndex; - update(); - - SendNotifyEvent(CLRGRDN_ACTIVE_KEY_CHANGE); -} - -///////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetSpline(ISplineInterpolator* pSpline, bool bRedraw) -{ - if (pSpline != m_pSpline) - { - //if (pSpline && pSpline->GetNumDimensions() != 3) - //return; - m_pSpline = pSpline; - m_nActiveKey = -1; - } - - ClearSelection(); - - if (bRedraw) - { - update(); - } -} - -////////////////////////////////////////////////////////////////////////// -ISplineInterpolator* CColorGradientCtrl::GetSpline() -{ - return m_pSpline; -} - -///////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::keyPressEvent(QKeyEvent* event) -{ - bool bProcessed = false; - - if (m_nActiveKey != -1 && m_pSpline) - { - switch (event->key()) - { - case Qt::Key_Delete: - { - RemoveKey(m_nActiveKey); - bProcessed = true; - } break; - case Qt::Key_Up: - { - CUndo undo("Move Spline Key"); - QPoint point = KeyToPoint(m_nActiveKey); - point.rx() -= 1; - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - TrackKey(point); - bProcessed = true; - } break; - case Qt::Key_Down: - { - CUndo undo("Move Spline Key"); - QPoint point = KeyToPoint(m_nActiveKey); - point.rx() += 1; - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - TrackKey(point); - bProcessed = true; - } break; - case Qt::Key_Left: - { - CUndo undo("Move Spline Key"); - QPoint point = KeyToPoint(m_nActiveKey); - point.rx() -= 1; - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - TrackKey(point); - bProcessed = true; - } break; - case Qt::Key_Right: - { - CUndo undo("Move Spline Key"); - QPoint point = KeyToPoint(m_nActiveKey); - point.rx() += 1; - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - TrackKey(point); - bProcessed = true; - } break; - - default: - break; //do nothing - } - - update(); - } - - event->setAccepted(bProcessed); -} - -////////////////////////////////////////////////////////////////////////////// -CColorGradientCtrl::EHitCode CColorGradientCtrl::HitTest(QPoint point) -{ - if (!m_pSpline) - { - return HIT_NOTHING; - } - - ISplineInterpolator::ValueType val; - float time; - PointToTimeValue(point, time, val); - - QRect rc = rect(); - - m_nHitKeyIndex = -1; - - if (rc.contains(point)) - { - m_nHitKeyDist = 0xFFFF; - m_hitCode = HIT_SPLINE; - - for (int i = 0; i < m_pSpline->GetKeyCount(); i++) - { - QPoint splinePt = TimeToPoint(m_pSpline->GetKeyTime(i)); - if (abs(point.x() - splinePt.x()) < abs(m_nHitKeyDist)) - { - m_nHitKeyIndex = i; - m_nHitKeyDist = point.x() - splinePt.x(); - } - } - if (abs(m_nHitKeyDist) < 4) - { - m_hitCode = HIT_KEY; - } - } - else - { - m_hitCode = HIT_NOTHING; - } - - return m_hitCode; -} - -/////////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::StartTracking() -{ - m_bTracking = true; - - GetIEditor()->BeginUndo(); - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - - setCursor(CMFCUtils::LoadCursor(IDC_ARRBLCKCROSS)); -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::TrackKey(QPoint point) -{ - if (point.x() < m_rcGradient.left() || point.y() > m_rcGradient.right()) - { - return; - } - - int nKey = m_nHitKeyIndex; - - if (nKey >= 0) - { - ISplineInterpolator::ValueType val; - float time; - PointToTimeValue(point, time, val); - - // Clamp to min/max time. - if (time < m_fMinTime || time > m_fMaxTime) - { - return; - } - - int i; - for (i = 0; i < m_pSpline->GetKeyCount(); i++) - { - // Switch to next key. - if ((m_pSpline->GetKeyTime(i) < time && i > nKey) || - (m_pSpline->GetKeyTime(i) > time && i < nKey)) - { - m_pSpline->SetKeyTime(nKey, time); - m_pSpline->Update(); - SetActiveKey(i); - m_nHitKeyIndex = i; - return; - } - } - - if (!m_bLockFirstLastKey || (nKey != 0 && nKey != m_pSpline->GetKeyCount() - 1)) - { - m_pSpline->SetKeyTime(nKey, time); - m_pSpline->Update(); - } - - SendNotifyEvent(CLRGRDN_CHANGE); - if (m_updateCallback) - { - m_updateCallback(this); - } - - update(); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::StopTracking(QPoint point) -{ - if (!m_bTracking) - { - return; - } - - GetIEditor()->AcceptUndo("Spline Move"); - - if (m_nHitKeyIndex >= 0) - { - QRect rc = rect(); - rc = rc.marginsAdded(QMargins(100, 100, 100, 100)); - if (!rc.contains(point)) - { - RemoveKey(m_nHitKeyIndex); - } - } - - m_bTracking = false; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::EditKey(int nKey) -{ - if (!m_pSpline) - { - return; - } - - if (nKey < 0 || nKey >= m_pSpline->GetKeyCount()) - { - return; - } - - SetActiveKey(nKey); - - ISplineInterpolator::ValueType val; - m_pSpline->GetKeyValue(nKey, val); - - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - - AzQtComponents::ColorPicker dlg(AzQtComponents::ColorPicker::Configuration::RGB); - dlg.setCurrentColor(ValueToColor(val)); - dlg.setSelectedColor(ValueToColor(val)); - connect(&dlg, &AzQtComponents::ColorPicker::currentColorChanged, this, &CColorGradientCtrl::OnKeyColorChanged); - if (dlg.exec() == QDialog::Accepted) - { - CUndo undo("Modify Gradient Color"); - OnKeyColorChanged(dlg.selectedColor()); - } - else - { - OnKeyColorChanged(ValueToColor(val)); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::OnKeyColorChanged(const AZ::Color& color) -{ - int nKey = m_nActiveKey; - if (!m_pSpline) - { - return; - } - if (nKey < 0 || nKey >= m_pSpline->GetKeyCount()) - { - return; - } - - ISplineInterpolator::ValueType val; - ColorToValue(color, val); - m_pSpline->SetKeyValue(nKey, val); - update(); - - if (m_bLockFirstLastKey) - { - if (nKey == 0) - { - m_pSpline->SetKeyValue(m_pSpline->GetKeyCount() - 1, val); - } - else if (nKey == m_pSpline->GetKeyCount() - 1) - { - m_pSpline->SetKeyValue(0, val); - } - } - m_pSpline->Update(); - SendNotifyEvent(CLRGRDN_CHANGE); - if (m_updateCallback) - { - m_updateCallback(this); - } - - GetIEditor()->UpdateViews(eRedrawViewports); -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::RemoveKey(int nKey) -{ - if (!m_pSpline) - { - return; - } - if (m_bLockFirstLastKey) - { - if (nKey == 0 || nKey == m_pSpline->GetKeyCount() - 1) - { - return; - } - } - - CUndo undo("Remove Spline Key"); - - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - m_nActiveKey = -1; - m_nHitKeyIndex = -1; - if (m_pSpline) - { - m_pSpline->RemoveKey(nKey); - m_pSpline->Update(); - } - SendNotifyEvent(CLRGRDN_CHANGE); - if (m_updateCallback) - { - m_updateCallback(this); - } - - update(); -} - -////////////////////////////////////////////////////////////////////////// -int CColorGradientCtrl::InsertKey(QPoint point) -{ - CUndo undo("Spline Insert Key"); - - ISplineInterpolator::ValueType val; - - float time; - PointToTimeValue(point, time, val); - - if (time < m_fMinTime || time > m_fMaxTime) - { - return -1; - } - - int i; - for (i = 0; i < m_pSpline->GetKeyCount(); i++) - { - // Skip if any key already have time that is very close. - if (fabs(m_pSpline->GetKeyTime(i) - time) < MIN_TIME_EPSILON) - { - return i; - } - } - - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - - m_pSpline->InsertKey(time, val); - m_pSpline->Interpolate(time, val); - ClearSelection(); - update(); - - SendNotifyEvent(CLRGRDN_CHANGE); - if (m_updateCallback) - { - m_updateCallback(this); - } - - for (i = 0; i < m_pSpline->GetKeyCount(); i++) - { - // Find key with added time. - if (m_pSpline->GetKeyTime(i) == time) - { - return i; - } - } - - return -1; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::ClearSelection() -{ - m_nActiveKey = -1; - if (m_pSpline) - { - m_bSelectedKeys.resize(m_pSpline->GetKeyCount()); - } - for (int i = 0; i < (int)m_bSelectedKeys.size(); i++) - { - m_bSelectedKeys[i] = false; - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetTimeMarker(float fTime) -{ - if (!m_pSpline) - { - return; - } - - { - QPoint pt = TimeToPoint(m_fTimeMarker); - QRect rc = QRect(pt.x(), m_rcGradient.top(), 0, m_rcGradient.bottom() - m_rcGradient.top()).normalized(); - rc += QMargins(1, 0, 1, 0); - update(rc); - } - { - QPoint pt = TimeToPoint(fTime); - QRect rc = QRect(pt.x(), m_rcGradient.top(), 0, m_rcGradient.bottom() - m_rcGradient.top()).normalized(); - rc += QMargins(1, 0, 1, 0); - update(rc); - } - m_fTimeMarker = fTime; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SendNotifyEvent(int nEvent) -{ - switch (nEvent) - { - case CLRGRDN_BEFORE_CHANGE: - emit beforeChange(); - break; - case CLRGRDN_CHANGE: - emit change(); - break; - case CLRGRDN_ACTIVE_KEY_CHANGE: - emit activeKeyChange(); - break; - } -} - -////////////////////////////////////////////////////////////////////////// -AZ::Color CColorGradientCtrl::ValueToColor(ISplineInterpolator::ValueType val) -{ - const AZ::Color color(val[0], val[1], val[2], 1.0); - return color.LinearToGamma(); -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::ColorToValue(const AZ::Color& col, ISplineInterpolator::ValueType& val) -{ - const AZ::Color colLin = col.GammaToLinear(); - val[0] = colLin.GetR(); - val[1] = colLin.GetG(); - val[2] = colLin.GetB(); - val[3] = 0; -} - -void CColorGradientCtrl::SetNoTimeMarker(bool noTimeMarker) -{ - m_bNoTimeMarker = noTimeMarker; - update(); -} - - -#include diff --git a/Code/Editor/Controls/ColorGradientCtrl.h b/Code/Editor/Controls/ColorGradientCtrl.h deleted file mode 100644 index bb1a83b0c1..0000000000 --- a/Code/Editor/Controls/ColorGradientCtrl.h +++ /dev/null @@ -1,167 +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 - * - */ - - -#ifndef CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H -#define CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H -#pragma once - -#if !defined(Q_MOC_RUN) -#include -#include -#include "Controls/WndGridHelper.h" -#endif - -namespace AZ -{ - class Color; -} - -// Notify event sent when spline is being modified. -#define CLRGRDN_CHANGE (0x0001) -// Notify event sent just before when spline is modified. -#define CLRGRDN_BEFORE_CHANGE (0x0002) -// Notify event sent when the active key changes -#define CLRGRDN_ACTIVE_KEY_CHANGE (0x0003) - -////////////////////////////////////////////////////////////////////////// -// Spline control. -////////////////////////////////////////////////////////////////////////// -class CColorGradientCtrl - : public QWidget -{ - Q_OBJECT -public: - CColorGradientCtrl(QWidget* parent = nullptr); - virtual ~CColorGradientCtrl(); - - //Key functions - int GetActiveKey() { return m_nActiveKey; }; - void SetActiveKey(int nIndex); - int InsertKey(QPoint point); - - // Turns on/off zooming and scroll support. - void SetNoZoom([[maybe_unused]] bool bNoZoom) { m_bNoZoom = false; }; - - void SetTimeRange(float tmin, float tmax) { m_fMinTime = tmin; m_fMaxTime = tmax; } - void SetValueRange(float tmin, float tmax) { m_fMinValue = tmin; m_fMaxValue = tmax; } - void SetTooltipValueScale(float x, float y) { m_fTooltipScaleX = x; m_fTooltipScaleY = y; }; - // Lock value of first and last key to be the same. - void LockFirstAndLastKeys(bool bLock) { m_bLockFirstLastKey = bLock; } - - void SetSpline(ISplineInterpolator* pSpline, bool bRedraw = false); - ISplineInterpolator* GetSpline(); - - void SetTimeMarker(float fTime); - - // Zoom in pixels per time unit. - void SetZoom(float fZoom); - void SetOrigin(float fOffset); - - typedef AZStd::function UpdateCallback; - void SetUpdateCallback(const UpdateCallback& cb) { m_updateCallback = cb; }; - - void SetNoTimeMarker(bool noTimeMarker); - -signals: - void change(); - void beforeChange(); - void activeKeyChange(); - -protected: - enum EHitCode - { - HIT_NOTHING, - HIT_KEY, - HIT_SPLINE, - }; - - void paintEvent(QPaintEvent* e) override; - void resizeEvent(QResizeEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - void mouseReleaseEvent(QMouseEvent* event) override; - void OnLButtonDown(QMouseEvent* event); - void mouseMoveEvent(QMouseEvent* event) override; - void OnLButtonUp(QMouseEvent* event); - void OnRButtonUp(QMouseEvent* event); - void mouseDoubleClickEvent(QMouseEvent* event) override; - void OnRButtonDown(QMouseEvent* event); - void keyPressEvent(QKeyEvent* event) override; - - // Drawing functions - void DrawGradient(QPaintEvent* e, QPainter* painter); - void DrawKeys(QPaintEvent* e, QPainter* painter); - void UpdateTooltip(QPoint pos); - - EHitCode HitTest(QPoint point); - - //Tracking support helper functions - void StartTracking(); - void TrackKey(QPoint point); - void StopTracking(QPoint point); - void RemoveKey(int nKey); - void EditKey(int nKey); - - QPoint KeyToPoint(int nKey); - QPoint TimeToPoint(float time); - void PointToTimeValue(QPoint point, float& time, ISplineInterpolator::ValueType& val); - float XOfsToTime(int x); - QPoint XOfsToPoint(int x); - - AZ::Color XOfsToColor(int x); - AZ::Color TimeToColor(float time); - - void ClearSelection(); - - void SendNotifyEvent(int nEvent); - - AZ::Color ValueToColor(ISplineInterpolator::ValueType val); - void ColorToValue(const AZ::Color& col, ISplineInterpolator::ValueType& val); - - -private: - void OnKeyColorChanged(const AZ::Color& color); - -private: - ISplineInterpolator* m_pSpline; - - bool m_bNoZoom; - - QRect m_rcClipRect; - QRect m_rcGradient; - QRect m_rcKeys; - - QPoint m_hitPoint; - EHitCode m_hitCode; - int m_nHitKeyIndex; - int m_nHitKeyDist; - QPoint m_curvePoint; - - float m_fTimeMarker; - - int m_nActiveKey; - int m_nKeyDrawRadius; - - bool m_bTracking; - - float m_fMinTime, m_fMaxTime; - float m_fMinValue, m_fMaxValue; - float m_fTooltipScaleX, m_fTooltipScaleY; - - bool m_bLockFirstLastKey; - - bool m_bNoTimeMarker; - - std::vector m_bSelectedKeys; - - UpdateCallback m_updateCallback; - - CWndGridHelper m_grid; -}; - -#endif // CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H diff --git a/Code/Editor/Controls/ReflectedPropertyControl/PropertyCtrl.cpp b/Code/Editor/Controls/ReflectedPropertyControl/PropertyCtrl.cpp index c228fbda09..68c4acb95e 100644 --- a/Code/Editor/Controls/ReflectedPropertyControl/PropertyCtrl.cpp +++ b/Code/Editor/Controls/ReflectedPropertyControl/PropertyCtrl.cpp @@ -27,7 +27,6 @@ void RegisterReflectedVarHandlers() EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew LocalStringPropertyHandler()); EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew LightAnimationPropertyHandler()); EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew UserPopupWidgetHandler()); - EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew ColorCurveHandler()); EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew FloatCurveHandler()); EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew MotionPropertyWidgetHandler()); } diff --git a/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.cpp b/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.cpp index b9f7e12e95..2278f55d95 100644 --- a/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.cpp +++ b/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.cpp @@ -170,30 +170,3 @@ bool FloatCurveHandler::ReadValuesIntoGUI([[maybe_unused]] size_t index, CSpline GUI->SetSpline(reinterpret_cast(instance.m_spline)); return false; } - - -QWidget* ColorCurveHandler::CreateGUI(QWidget *pParent) -{ - CColorGradientCtrl* gradientCtrl = new CColorGradientCtrl(pParent); - //connect(gradientCtrl, &CColorGradientCtrl::change, [gradientCtrl]() - //{ - // EBUS_EVENT(AzToolsFramework::PropertyEditorGUIMessages::Bus, RequestWrite, gradientCtrl); - //}); - gradientCtrl->SetTimeRange(0, 1); - gradientCtrl->setFixedHeight(36); - return gradientCtrl; - -} - -void ColorCurveHandler::ConsumeAttribute(CColorGradientCtrl*, AZ::u32, AzToolsFramework::PropertyAttributeReader*, const char*) -{} - -void ColorCurveHandler::WriteGUIValuesIntoProperty([[maybe_unused]] size_t index, [[maybe_unused]] CColorGradientCtrl* GUI, [[maybe_unused]] property_t& instance, [[maybe_unused]] AzToolsFramework::InstanceDataNode* node) -{} - -bool ColorCurveHandler::ReadValuesIntoGUI([[maybe_unused]] size_t index, CColorGradientCtrl* GUI, const property_t& instance, [[maybe_unused]] AzToolsFramework::InstanceDataNode* node) -{ - GUI->SetSpline(reinterpret_cast(instance.m_spline)); - return false; -} - diff --git a/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h b/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h index e63a974c91..5ec24b679d 100644 --- a/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h +++ b/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h @@ -16,7 +16,6 @@ #include #include "ReflectedVar.h" #include "Util/VariablePropertyType.h" -#include "Controls/ColorGradientCtrl.h" #include "Controls/SplineCtrl.h" #include #endif @@ -82,17 +81,4 @@ public: void OnSplineChange(CSplineCtrl*); }; -class ColorCurveHandler : public QObject, public AzToolsFramework::PropertyHandler < CReflectedVarSpline, CColorGradientCtrl> -{ -public: - AZ_CLASS_ALLOCATOR(ColorCurveHandler, AZ::SystemAllocator, 0); - bool IsDefaultHandler() const override { return false; } - QWidget* CreateGUI(QWidget *pParent) override; - - AZ::u32 GetHandlerName(void) const override { return AZ_CRC("ePropertyColorCurve", 0xa30da4ec); } - - void ConsumeAttribute(CColorGradientCtrl* GUI, AZ::u32 attrib, AzToolsFramework::PropertyAttributeReader* attrValue, const char* debugName) override; - void WriteGUIValuesIntoProperty(size_t index, CColorGradientCtrl* GUI, property_t& instance, AzToolsFramework::InstanceDataNode* node) override; - bool ReadValuesIntoGUI(size_t index, CColorGradientCtrl* GUI, const property_t& instance, AzToolsFramework::InstanceDataNode* node) override; -}; #endif // CRYINCLUDE_EDITOR_UTILS_PROPERTYMISCCTRL_H diff --git a/Code/Editor/Core/QtEditorApplication.cpp b/Code/Editor/Core/QtEditorApplication.cpp index a4aab24be4..2439228476 100644 --- a/Code/Editor/Core/QtEditorApplication.cpp +++ b/Code/Editor/Core/QtEditorApplication.cpp @@ -16,18 +16,11 @@ #include #include #include -#if defined(AZ_PLATFORM_WINDOWS) -#include -#include -#endif + #include #include #include -// AzFramework -#if defined(AZ_PLATFORM_WINDOWS) -# include -#endif // defined(AZ_PLATFORM_WINDOWS) // AzQtComponents #include @@ -39,7 +32,6 @@ #include "Settings.h" #include "CryEdit.h" - enum { // in milliseconds @@ -241,7 +233,6 @@ namespace Editor EditorQtApplication::EditorQtApplication(int& argc, char** argv) : AzQtApplication(argc, argv) - , m_inWinEventFilter(false) , m_stylesheet(new AzQtComponents::O3DEStylesheet(this)) , m_idleTimer(new QTimer(this)) { @@ -368,86 +359,10 @@ namespace Editor UninstallEditorTranslators(); } -#if defined(AZ_PLATFORM_WINDOWS) - bool EditorQtApplication::nativeEventFilter([[maybe_unused]] const QByteArray& eventType, void* message, long* result) + EditorQtApplication* EditorQtApplication::instance() { - MSG* msg = (MSG*)message; - - if (msg->message == WM_MOVING || msg->message == WM_SIZING) - { - m_isMovingOrResizing = true; - } - else if (msg->message == WM_EXITSIZEMOVE) - { - m_isMovingOrResizing = false; - } - - // Prevent the user from being able to move the window in game mode. - // This is done during the hit test phase to bypass the native window move messages. If the window - // decoration wrapper title bar contains the cursor, set the result to HTCLIENT instead of - // HTCAPTION. - if (msg->message == WM_NCHITTEST && GetIEditor()->IsInGameMode()) - { - const LRESULT defWinProcResult = DefWindowProc(msg->hwnd, msg->message, msg->wParam, msg->lParam); - if (defWinProcResult == 1) - { - if (QWidget* widget = QWidget::find((WId)msg->hwnd)) - { - if (auto wrapper = qobject_cast(widget)) - { - AzQtComponents::TitleBar* titleBar = wrapper->titleBar(); - const short global_x = static_cast(LOWORD(msg->lParam)); - const short global_y = static_cast(HIWORD(msg->lParam)); - - const QPoint globalPos = QHighDpi::fromNativePixels(QPoint(global_x, global_y), widget->window()->windowHandle()); - const QPoint local = titleBar->mapFromGlobal(globalPos); - if (titleBar->draggableRect().contains(local) && !titleBar->isTopResizeArea(globalPos)) - { - *result = HTCLIENT; - return true; - } - } - } - } - } - - // Ensure that the Windows WM_INPUT messages get passed through to the AzFramework input system. - // These events are only broadcast in game mode. In Editor mode, RenderViewportWidget creates synthetic - // keyboard and mouse events via Qt. - if (GetIEditor()->IsInGameMode()) - { - if (msg->message == WM_INPUT) - { - UINT rawInputSize; - const UINT rawInputHeaderSize = sizeof(RAWINPUTHEADER); - GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, nullptr, &rawInputSize, rawInputHeaderSize); - - AZStd::array rawInputBytesArray; - LPBYTE rawInputBytes = rawInputBytesArray.data(); - - [[maybe_unused]] const UINT bytesCopied = GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, rawInputBytes, &rawInputSize, rawInputHeaderSize); - CRY_ASSERT(bytesCopied == rawInputSize); - - RAWINPUT* rawInput = (RAWINPUT*)rawInputBytes; - CRY_ASSERT(rawInput); - - AzFramework::RawInputNotificationBusWindows::Broadcast(&AzFramework::RawInputNotificationsWindows::OnRawInputEvent, *rawInput); - - return false; - } - else if (msg->message == WM_DEVICECHANGE) - { - if (msg->wParam == 0x0007) // DBT_DEVNODES_CHANGED - { - AzFramework::RawInputNotificationBusWindows::Broadcast(&AzFramework::RawInputNotificationsWindows::OnRawInputDeviceChangeEvent); - } - return true; - } - } - - return false; + return static_cast(QApplication::instance()); } -#endif void EditorQtApplication::OnEditorNotifyEvent(EEditorNotifyEvent event) { @@ -505,11 +420,6 @@ namespace Editor return m_stylesheet->GetColorByName(name); } - EditorQtApplication* EditorQtApplication::instance() - { - return static_cast(QApplication::instance()); - } - bool EditorQtApplication::IsActive() { return applicationState() == Qt::ApplicationActive; @@ -613,42 +523,6 @@ namespace Editor case QEvent::KeyRelease: m_pressedKeys.remove(reinterpret_cast(event)->key()); break; -#ifdef AZ_PLATFORM_WINDOWS - case QEvent::Leave: - { - // if we receive a leave event for a toolbar on Windows - // check first whether we really left it. If we didn't: start checking - // for the tool bar under the mouse by timer to check when we really left. - // Synthesize a new leave event then. Workaround for LY-69788 - auto toolBarAt = [](const QPoint& pos) -> QToolBar* { - QWidget* widget = qApp->widgetAt(pos); - while (widget != nullptr) - { - if (QToolBar* tb = qobject_cast(widget)) - { - return tb; - } - widget = widget->parentWidget(); - } - return nullptr; - }; - if (object == toolBarAt(QCursor::pos())) - { - QTimer* t = new QTimer(object); - t->start(100); - connect(t, &QTimer::timeout, object, [t, object, toolBarAt]() { - if (object != toolBarAt(QCursor::pos())) - { - QEvent event(QEvent::Leave); - qApp->sendEvent(object, &event); - t->deleteLater(); - } - }); - return true; - } - break; - } -#endif default: break; } diff --git a/Code/Editor/Core/QtEditorApplication.h b/Code/Editor/Core/QtEditorApplication.h index 2e3612095e..0d702bf647 100644 --- a/Code/Editor/Core/QtEditorApplication.h +++ b/Code/Editor/Core/QtEditorApplication.h @@ -72,14 +72,12 @@ namespace Editor //// static EditorQtApplication* instance(); + static EditorQtApplication* newInstance(int& argc, char** argv); static bool IsActive(); bool isMovingOrResizing() const; - // QAbstractNativeEventFilter: - bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override; - // IEditorNotifyListener: void OnEditorNotifyEvent(EEditorNotifyEvent event) override; @@ -100,6 +98,10 @@ namespace Editor signals: void skinChanged(); + protected: + + bool m_isMovingOrResizing = false; + private: enum TimerResetFlag { @@ -116,8 +118,6 @@ namespace Editor AzQtComponents::O3DEStylesheet* m_stylesheet; - bool m_inWinEventFilter = false; - // Translators void InstallEditorTranslators(); void UninstallEditorTranslators(); @@ -127,7 +127,6 @@ namespace Editor QTranslator* m_editorTranslator = nullptr; QTranslator* m_assetBrowserTranslator = nullptr; QTimer* const m_idleTimer = nullptr; - bool m_isMovingOrResizing = false; AZ::UserSettingsProvider m_localUserSettings; diff --git a/Code/Editor/CryEdit.cpp b/Code/Editor/CryEdit.cpp index 027198ac80..e4138da932 100644 --- a/Code/Editor/CryEdit.cpp +++ b/Code/Editor/CryEdit.cpp @@ -287,21 +287,22 @@ bool CCryDocManager::DoPromptFileName(QString& fileName, [[maybe_unused]] UINT n return false; } -CCryEditDoc* CCryDocManager::OpenDocumentFile(const char* lpszFileName, bool bAddToMRU) +CCryEditDoc* CCryDocManager::OpenDocumentFile(const char* filename, bool addToMostRecentFileList, COpenSameLevelOptions openSameLevelOptions) { - assert(lpszFileName != nullptr); + assert(filename != nullptr); + const bool reopenIfSame = openSameLevelOptions == COpenSameLevelOptions::ReopenLevelIfSame; // find the highest confidence auto pos = m_templateList.begin(); CCrySingleDocTemplate::Confidence bestMatch = CCrySingleDocTemplate::noAttempt; CCrySingleDocTemplate* pBestTemplate = nullptr; CCryEditDoc* pOpenDocument = nullptr; - if (lpszFileName[0] == '\"') + if (filename[0] == '\"') { - ++lpszFileName; + ++filename; } - QString szPath = QString::fromUtf8(lpszFileName); + QString szPath = QString::fromUtf8(filename); if (szPath.endsWith('"')) { szPath.remove(szPath.length() - 1, 1); @@ -325,7 +326,7 @@ CCryEditDoc* CCryDocManager::OpenDocumentFile(const char* lpszFileName, bool bAd } } - if (pOpenDocument != nullptr) + if (!reopenIfSame && pOpenDocument != nullptr) { return pOpenDocument; } @@ -336,7 +337,7 @@ CCryEditDoc* CCryDocManager::OpenDocumentFile(const char* lpszFileName, bool bAd return nullptr; } - return pBestTemplate->OpenDocumentFile(szPath.toUtf8().data(), bAddToMRU, false); + return pBestTemplate->OpenDocumentFile(szPath.toUtf8().data(), addToMostRecentFileList, false); } ////////////////////////////////////////////////////////////////////////////// @@ -818,7 +819,7 @@ CCryEditDoc* CCrySingleDocTemplate::OpenDocumentFile(const char* lpszPathName, b return OpenDocumentFile(lpszPathName, true, bMakeVisible); } -CCryEditDoc* CCrySingleDocTemplate::OpenDocumentFile(const char* lpszPathName, bool bAddToMRU, [[maybe_unused]] bool bMakeVisible) +CCryEditDoc* CCrySingleDocTemplate::OpenDocumentFile(const char* lpszPathName, bool addToMostRecentFileList, [[maybe_unused]] bool bMakeVisible) { CCryEditDoc* pCurDoc = GetIEditor()->GetDocument(); @@ -848,7 +849,7 @@ CCryEditDoc* CCrySingleDocTemplate::OpenDocumentFile(const char* lpszPathName, b { pCurDoc->OnOpenDocument(lpszPathName); pCurDoc->SetPathName(lpszPathName); - if (bAddToMRU) + if (addToMostRecentFileList) { CCryEditApp::instance()->AddToRecentFileList(lpszPathName); } @@ -2631,7 +2632,7 @@ void CCryEditApp::OnShowHelpers() void CCryEditApp::OnEditLevelData() { auto dir = QFileInfo(GetIEditor()->GetDocument()->GetLevelPathName()).dir(); - CFileUtil::EditTextFile(dir.absoluteFilePath("LevelData.xml").toUtf8().data()); + CFileUtil::EditTextFile(dir.absoluteFilePath("leveldata.xml").toUtf8().data()); } ////////////////////////////////////////////////////////////////////////// @@ -3365,7 +3366,7 @@ void CCryEditApp::OnOpenSlice() } ////////////////////////////////////////////////////////////////////////// -CCryEditDoc* CCryEditApp::OpenDocumentFile(const char* lpszFileName) +CCryEditDoc* CCryEditApp::OpenDocumentFile(const char* filename, bool addToMostRecentFileList, COpenSameLevelOptions openSameLevelOptions) { if (m_openingLevel) { @@ -3405,9 +3406,9 @@ CCryEditDoc* CCryEditApp::OpenDocumentFile(const char* lpszFileName) openDocTraceHandler.SetShowWindow(false); } - // in this case, we set bAddToMRU to always be true because adding files to the MRU list + // in this case, we set addToMostRecentFileList to always be true because adding files to the MRU list // automatically culls duplicate and normalizes paths anyway - m_pDocManager->OpenDocumentFile(lpszFileName, true); + m_pDocManager->OpenDocumentFile(filename, addToMostRecentFileList, openSameLevelOptions); if (openDocTraceHandler.HasAnyErrors()) { @@ -4134,9 +4135,9 @@ extern "C" int AZ_DLL_EXPORT CryEditMain(int argc, char* argv[]) Editor::EditorQtApplication::InstallQtLogHandler(); AzQtComponents::Utilities::HandleDpiAwareness(AzQtComponents::Utilities::SystemDpiAware); - Editor::EditorQtApplication app(argc, argv); + Editor::EditorQtApplication* app = Editor::EditorQtApplication::newInstance(argc, argv); - if (app.arguments().contains("-autotest_mode")) + if (app->arguments().contains("-autotest_mode")) { // Nullroute all stdout to null for automated tests, this way we make sure // that the test result output is not polluted with unrelated output data. @@ -4172,12 +4173,7 @@ extern "C" int AZ_DLL_EXPORT CryEditMain(int argc, char* argv[]) return -1; } - AzToolsFramework::EditorEvents::Bus::Broadcast(&AzToolsFramework::EditorEvents::NotifyQtApplicationAvailable, &app); - - #if defined(AZ_PLATFORM_MAC) - // Native menu bars do not work on macOS due to all the tool dialogs - QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar); - #endif + AzToolsFramework::EditorEvents::Bus::Broadcast(&AzToolsFramework::EditorEvents::NotifyQtApplicationAvailable, app); int exitCode = 0; @@ -4188,9 +4184,9 @@ extern "C" int AZ_DLL_EXPORT CryEditMain(int argc, char* argv[]) if (didCryEditStart) { - app.EnableOnIdle(); + app->EnableOnIdle(); - ret = app.exec(); + ret = app->exec(); } else { @@ -4201,6 +4197,8 @@ extern "C" int AZ_DLL_EXPORT CryEditMain(int argc, char* argv[]) } + delete app; + gSettings.Disconnect(); return ret; diff --git a/Code/Editor/CryEdit.h b/Code/Editor/CryEdit.h index 730af7c034..f68cfdc33d 100644 --- a/Code/Editor/CryEdit.h +++ b/Code/Editor/CryEdit.h @@ -85,6 +85,12 @@ public: using EditorIdleProcessingBus = AZ::EBus; +enum class COpenSameLevelOptions +{ + ReopenLevelIfSame, + NotReopenIfSame +}; + AZ_PUSH_DISABLE_DLL_EXPORT_BASECLASS_WARNING AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING class SANDBOX_API CCryEditApp @@ -174,7 +180,9 @@ public: virtual bool InitInstance(); virtual int ExitInstance(int exitCode = 0); virtual bool OnIdle(LONG lCount); - virtual CCryEditDoc* OpenDocumentFile(const char* lpszFileName); + virtual CCryEditDoc* OpenDocumentFile(const char* filename, + bool addToMostRecentFileList=true, + COpenSameLevelOptions openSameLevelOptions = COpenSameLevelOptions::NotReopenIfSame); CCryDocManager* GetDocManager() { return m_pDocManager; } @@ -448,7 +456,7 @@ public: ~CCrySingleDocTemplate() {}; // avoid creating another CMainFrame // close other type docs before opening any things - virtual CCryEditDoc* OpenDocumentFile(const char* lpszPathName, bool bAddToMRU, bool bMakeVisible); + virtual CCryEditDoc* OpenDocumentFile(const char* lpszPathName, bool addToMostRecentFileList, bool bMakeVisible); virtual CCryEditDoc* OpenDocumentFile(const char* lpszPathName, bool bMakeVisible = TRUE); virtual Confidence MatchDocType(const char* lpszPathName, CCryEditDoc*& rpDocMatch); @@ -468,7 +476,7 @@ public: virtual void OnFileNew(); virtual bool DoPromptFileName(QString& fileName, UINT nIDSTitle, DWORD lFlags, bool bOpenFileDialog, CDocTemplate* pTemplate); - virtual CCryEditDoc* OpenDocumentFile(const char* lpszFileName, bool bAddToMRU); + virtual CCryEditDoc* OpenDocumentFile(const char* filename, bool addToMostRecentFileList, COpenSameLevelOptions openSameLevelOptions = COpenSameLevelOptions::NotReopenIfSame); QVector m_templateList; }; diff --git a/Code/Editor/CryEditPy.cpp b/Code/Editor/CryEditPy.cpp index 1b38fe23b8..a25a497a4e 100644 --- a/Code/Editor/CryEditPy.cpp +++ b/Code/Editor/CryEditPy.cpp @@ -143,20 +143,11 @@ namespace return false; } } + const bool addToMostRecentFileList = false; + auto newDocument = CCryEditApp::instance()->OpenDocumentFile(levelPath.toUtf8().data(), + addToMostRecentFileList, COpenSameLevelOptions::ReopenLevelIfSame); - auto previousDocument = GetIEditor()->GetDocument(); - QString previousPathName = (previousDocument != nullptr) ? previousDocument->GetLevelPathName() : ""; - auto newDocument = CCryEditApp::instance()->OpenDocumentFile(levelPath.toUtf8().data()); - - // the underlying document pointer doesn't change, so we can't check that; use the path name's instead - - bool result = true; - if (newDocument == nullptr || newDocument->IsLevelLoadFailed() || (newDocument->GetLevelPathName() == previousPathName)) - { - result = false; - } - - return result; + return newDocument != nullptr && !newDocument->IsLevelLoadFailed(); } bool PyOpenLevelNoPrompt(const char* pLevelName) @@ -407,6 +398,11 @@ inline namespace Commands { return AZ::Debug::Trace::WaitForDebugger(timeoutSeconds); } + + AZStd::string PyGetFileAlias(AZStd::string alias) + { + return AZ::IO::FileIOBase::GetInstance()->GetAlias(alias.c_str()); + } } namespace AzToolsFramework @@ -457,6 +453,8 @@ namespace AzToolsFramework addLegacyGeneral(behaviorContext->Method("attach_debugger", PyAttachDebugger, nullptr, "Prompts for attaching the debugger")); addLegacyGeneral(behaviorContext->Method("wait_for_debugger", PyWaitForDebugger, behaviorContext->MakeDefaultValues(-1.f), "Pauses this thread execution until the debugger has been attached")); + addLegacyGeneral(behaviorContext->Method("get_file_alias", PyGetFileAlias, nullptr, "Retrieves path for IO alias")); + // this will put these methods into the 'azlmbr.legacy.checkout_dialog' module auto addCheckoutDialog = [](AZ::BehaviorContext::GlobalMethodBuilder methodBuilder) { diff --git a/Code/Editor/GameExporter.cpp b/Code/Editor/GameExporter.cpp index 0324629877..ae1095b03a 100644 --- a/Code/Editor/GameExporter.cpp +++ b/Code/Editor/GameExporter.cpp @@ -31,11 +31,11 @@ #include ////////////////////////////////////////////////////////////////////////// -#define MUSIC_LEVEL_LIBRARY_FILE "Music.xml" -#define MATERIAL_LEVEL_LIBRARY_FILE "Materials.xml" -#define RESOURCE_LIST_FILE "ResourceList.txt" -#define USED_RESOURCE_LIST_FILE "UsedResourceList.txt" -#define SHADER_LIST_FILE "ShadersList.txt" +#define MUSIC_LEVEL_LIBRARY_FILE "music.xml" +#define MATERIAL_LEVEL_LIBRARY_FILE "materials.xml" +#define RESOURCE_LIST_FILE "resourcelist.txt" +#define USED_RESOURCE_LIST_FILE "usedresourcelist.txt" +#define SHADER_LIST_FILE "shaderslist.txt" #define GetAValue(rgb) ((BYTE)((rgb) >> 24)) @@ -185,9 +185,9 @@ bool CGameExporter::Export(unsigned int flags, [[maybe_unused]] EEndian eExportE ExportOcclusionMesh(sLevelPath.toUtf8().data()); //! Export Level data. - CLogFile::WriteLine("Exporting LevelData.xml"); + CLogFile::WriteLine("Exporting leveldata.xml"); ExportLevelData(sLevelPath); - CLogFile::WriteLine("Exporting LevelData.xml done."); + CLogFile::WriteLine("Exporting leveldata.xml done."); ExportLevelInfo(sLevelPath); @@ -266,26 +266,26 @@ void CGameExporter::ExportOcclusionMesh(const char* pszGamePath) void CGameExporter::ExportLevelData(const QString& path, bool /*bExportMission*/) { IEditor* pEditor = GetIEditor(); - pEditor->SetStatusText(QObject::tr("Exporting LevelData.xml...")); + pEditor->SetStatusText(QObject::tr("Exporting leveldata.xml...")); char versionString[256]; pEditor->GetFileVersion().ToString(versionString); - XmlNodeRef root = XmlHelpers::CreateXmlNode("LevelData"); + XmlNodeRef root = XmlHelpers::CreateXmlNode("leveldata"); root->setAttr("SandboxVersion", versionString); - XmlNodeRef rootAction = XmlHelpers::CreateXmlNode("LevelDataAction"); + XmlNodeRef rootAction = XmlHelpers::CreateXmlNode("leveldataaction"); rootAction->setAttr("SandboxVersion", versionString); ////////////////////////////////////////////////////////////////////////// // Save Level Data XML ////////////////////////////////////////////////////////////////////////// - QString levelDataFile = path + "LevelData.xml"; + QString levelDataFile = path + "leveldata.xml"; XmlString xmlData = root->getXML(); CCryMemFile file; file.Write(xmlData.c_str(), static_cast(xmlData.length())); m_levelPak.m_pakFile.UpdateFile(levelDataFile.toUtf8().data(), file); - QString levelDataActionFile = path + "LevelDataAction.xml"; + QString levelDataActionFile = path + "leveldataaction.xml"; XmlString xmlDataAction = rootAction->getXML(); CCryMemFile fileAction; fileAction.Write(xmlDataAction.c_str(), static_cast(xmlDataAction.length())); @@ -298,7 +298,7 @@ void CGameExporter::ExportLevelData(const QString& path, bool /*bExportMission*/ if (savedEntities) { QString entitiesFile; - entitiesFile = QStringLiteral("%1%2.entities_xml").arg(path, "Mission0"); + entitiesFile = QStringLiteral("%1%2.entities_xml").arg(path, "mission0"); m_levelPak.m_pakFile.UpdateFile(entitiesFile.toUtf8().data(), entitySaveBuffer.begin(), static_cast(entitySaveBuffer.size())); } } @@ -326,7 +326,7 @@ void CGameExporter::ExportLevelInfo(const QString& path) ////////////////////////////////////////////////////////////////////////// // Save LevelInfo file. ////////////////////////////////////////////////////////////////////////// - QString filename = path + "LevelInfo.xml"; + QString filename = path + "levelinfo.xml"; XmlString xmlData = root->getXML(); CCryMemFile file; diff --git a/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp b/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp index ea3653f663..275df11784 100644 --- a/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp +++ b/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp @@ -38,6 +38,8 @@ namespace UnitTest void BeginCursorCapture() override; void EndCursorCapture() override; bool IsMouseOver() const override; + void SetOverrideCursor(AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride) override; + void ClearOverrideCursor() override; private: AzToolsFramework::QtEventToAzInputMapper* m_inputChannelMapper = nullptr; @@ -58,6 +60,17 @@ namespace UnitTest return true; } + void ViewportMouseCursorRequestImpl::SetOverrideCursor( + [[maybe_unused]] AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride) + { + // noop + } + + void ViewportMouseCursorRequestImpl::ClearOverrideCursor() + { + // noop + } + class ModularViewportCameraControllerFixture : public AllocatorsTestFixture { public: diff --git a/Code/Editor/Core/QtEditorApplication_linux.cpp b/Code/Editor/Platform/Linux/Editor/Core/QtEditorApplication_linux.cpp similarity index 73% rename from Code/Editor/Core/QtEditorApplication_linux.cpp rename to Code/Editor/Platform/Linux/Editor/Core/QtEditorApplication_linux.cpp index 5491134170..ad5e57479b 100644 --- a/Code/Editor/Core/QtEditorApplication_linux.cpp +++ b/Code/Editor/Platform/Linux/Editor/Core/QtEditorApplication_linux.cpp @@ -6,7 +6,7 @@ * */ -#include "QtEditorApplication.h" +#include "QtEditorApplication_linux.h" #ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB #include @@ -14,7 +14,16 @@ namespace Editor { - bool EditorQtApplication::nativeEventFilter([[maybe_unused]] const QByteArray& eventType, void* message, long*) + EditorQtApplication* EditorQtApplication::newInstance(int& argc, char** argv) + { +#ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB + return new EditorQtApplicationXcb(argc, argv); +#endif + + return nullptr; + } + + bool EditorQtApplicationXcb::nativeEventFilter([[maybe_unused]] const QByteArray& eventType, void* message, long*) { if (GetIEditor()->IsInGameMode()) { diff --git a/Code/Editor/Platform/Linux/Editor/Core/QtEditorApplication_linux.h b/Code/Editor/Platform/Linux/Editor/Core/QtEditorApplication_linux.h new file mode 100644 index 0000000000..8c145c3aa7 --- /dev/null +++ b/Code/Editor/Platform/Linux/Editor/Core/QtEditorApplication_linux.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +namespace Editor +{ + class EditorQtApplicationXcb : public EditorQtApplication + { + Q_OBJECT + public: + EditorQtApplicationXcb(int& argc, char** argv) + : EditorQtApplication(argc, argv) + { + } + + // QAbstractNativeEventFilter: + bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override; + }; +} // namespace Editor diff --git a/Code/Editor/Platform/Linux/platform_linux_files.cmake b/Code/Editor/Platform/Linux/platform_linux_files.cmake index 3baed702c2..875acad1c3 100644 --- a/Code/Editor/Platform/Linux/platform_linux_files.cmake +++ b/Code/Editor/Platform/Linux/platform_linux_files.cmake @@ -7,6 +7,6 @@ # set(FILES - ../../Core/QtEditorApplication_linux.cpp + Editor/Core/QtEditorApplication_linux.cpp ../Common/Unimplemented/Util/Mailer_Unimplemented.cpp ) diff --git a/Code/Editor/Platform/Mac/Editor/Core/QtEditorApplication_mac.h b/Code/Editor/Platform/Mac/Editor/Core/QtEditorApplication_mac.h new file mode 100644 index 0000000000..2de7514bbf --- /dev/null +++ b/Code/Editor/Platform/Mac/Editor/Core/QtEditorApplication_mac.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +namespace Editor +{ + class EditorQtApplicationMac : public EditorQtApplication + { + Q_OBJECT + public: + EditorQtApplicationMac(int& argc, char** argv) + : EditorQtApplication(argc, argv) + { + } + + // QAbstractNativeEventFilter: + bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override; + }; +} // namespace Editor diff --git a/Code/Editor/Core/QtEditorApplication_mac.mm b/Code/Editor/Platform/Mac/Editor/Core/QtEditorApplication_mac.mm similarity index 78% rename from Code/Editor/Core/QtEditorApplication_mac.mm rename to Code/Editor/Platform/Mac/Editor/Core/QtEditorApplication_mac.mm index 17ad8f7ffd..a7f59b7ac8 100644 --- a/Code/Editor/Core/QtEditorApplication_mac.mm +++ b/Code/Editor/Platform/Mac/Editor/Core/QtEditorApplication_mac.mm @@ -19,7 +19,14 @@ namespace Editor { - bool EditorQtApplication::nativeEventFilter(const QByteArray& eventType, void* message, long* result) + EditorQtApplication* EditorQtApplication::newInstance(int& argc, char** argv) + { + QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar); + + return new EditorQtApplicationMac(argc, argv); + } + + bool EditorQtApplicationMac::nativeEventFilter(const QByteArray& eventType, void* message, long* result) { NSEvent* event = (NSEvent*)message; if (GetIEditor()->IsInGameMode()) diff --git a/Code/Editor/Platform/Mac/platform_mac_files.cmake b/Code/Editor/Platform/Mac/platform_mac_files.cmake index 91a0bed574..5549eaf2f9 100644 --- a/Code/Editor/Platform/Mac/platform_mac_files.cmake +++ b/Code/Editor/Platform/Mac/platform_mac_files.cmake @@ -7,7 +7,7 @@ # set(FILES - ../../Core/QtEditorApplication_mac.mm + Editor/Core/QtEditorApplication_mac.mm ../../LogFile_mac.mm ../../WindowObserver_mac.h ../../WindowObserver_mac.mm diff --git a/Code/Editor/Platform/Windows/Editor/Core/QtEditorApplication_windows.cpp b/Code/Editor/Platform/Windows/Editor/Core/QtEditorApplication_windows.cpp new file mode 100644 index 0000000000..f8065af931 --- /dev/null +++ b/Code/Editor/Platform/Windows/Editor/Core/QtEditorApplication_windows.cpp @@ -0,0 +1,165 @@ +/* + * 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 "QtEditorApplication_windows.h" + +// Qt +#include +#include +#include +#include +#include + +#include +#include + +// AzQtComponents +#include +#include + +// AzFramework +#include + +namespace Editor +{ + EditorQtApplication* EditorQtApplication::newInstance(int& argc, char** argv) + { + return new EditorQtApplicationWindows(argc, argv); + } + + bool EditorQtApplicationWindows::nativeEventFilter([[maybe_unused]] const QByteArray& eventType, void* message, long* result) + { + MSG* msg = (MSG*)message; + + if (msg->message == WM_MOVING || msg->message == WM_SIZING) + { + m_isMovingOrResizing = true; + } + else if (msg->message == WM_EXITSIZEMOVE) + { + m_isMovingOrResizing = false; + } + + // Prevent the user from being able to move the window in game mode. + // This is done during the hit test phase to bypass the native window move messages. If the window + // decoration wrapper title bar contains the cursor, set the result to HTCLIENT instead of + // HTCAPTION. + if (msg->message == WM_NCHITTEST && GetIEditor()->IsInGameMode()) + { + const LRESULT defWinProcResult = DefWindowProc(msg->hwnd, msg->message, msg->wParam, msg->lParam); + if (defWinProcResult == 1) + { + if (QWidget* widget = QWidget::find((WId)msg->hwnd)) + { + if (auto wrapper = qobject_cast(widget)) + { + AzQtComponents::TitleBar* titleBar = wrapper->titleBar(); + const short global_x = static_cast(LOWORD(msg->lParam)); + const short global_y = static_cast(HIWORD(msg->lParam)); + + const QPoint globalPos = QHighDpi::fromNativePixels(QPoint(global_x, global_y), widget->window()->windowHandle()); + const QPoint local = titleBar->mapFromGlobal(globalPos); + if (titleBar->draggableRect().contains(local) && !titleBar->isTopResizeArea(globalPos)) + { + *result = HTCLIENT; + return true; + } + } + } + } + } + + // Ensure that the Windows WM_INPUT messages get passed through to the AzFramework input system. + // These events are only broadcast in game mode. In Editor mode, RenderViewportWidget creates synthetic + // keyboard and mouse events via Qt. + if (GetIEditor()->IsInGameMode()) + { + if (msg->message == WM_INPUT) + { + UINT rawInputSize; + const UINT rawInputHeaderSize = sizeof(RAWINPUTHEADER); + GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, nullptr, &rawInputSize, rawInputHeaderSize); + + AZStd::array rawInputBytesArray; + LPBYTE rawInputBytes = rawInputBytesArray.data(); + + [[maybe_unused]] const UINT bytesCopied = + GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, rawInputBytes, &rawInputSize, rawInputHeaderSize); + CRY_ASSERT(bytesCopied == rawInputSize); + + RAWINPUT* rawInput = (RAWINPUT*)rawInputBytes; + CRY_ASSERT(rawInput); + + AzFramework::RawInputNotificationBusWindows::Broadcast( + &AzFramework::RawInputNotificationsWindows::OnRawInputEvent, *rawInput); + + return false; + } + else if (msg->message == WM_DEVICECHANGE) + { + if (msg->wParam == 0x0007) // DBT_DEVNODES_CHANGED + { + AzFramework::RawInputNotificationBusWindows::Broadcast( + &AzFramework::RawInputNotificationsWindows::OnRawInputDeviceChangeEvent); + } + return true; + } + } + + return false; + } + + bool EditorQtApplicationWindows::eventFilter(QObject* object, QEvent* event) + { + switch (event->type()) + { + case QEvent::Leave: + { + // if we receive a leave event for a toolbar on Windows + // check first whether we really left it. If we didn't: start checking + // for the tool bar under the mouse by timer to check when we really left. + // Synthesize a new leave event then. Workaround for LY-69788 + auto toolBarAt = [](const QPoint& pos) -> QToolBar* + { + QWidget* widget = qApp->widgetAt(pos); + while (widget != nullptr) + { + if (QToolBar* tb = qobject_cast(widget)) + { + return tb; + } + widget = widget->parentWidget(); + } + return false; + }; + if (object == toolBarAt(QCursor::pos())) + { + QTimer* t = new QTimer(object); + t->start(100); + connect( + t, &QTimer::timeout, object, + [t, object, toolBarAt]() + { + if (object != toolBarAt(QCursor::pos())) + { + QEvent event(QEvent::Leave); + qApp->sendEvent(object, &event); + t->deleteLater(); + } + }); + return true; + } + break; + } + default: + break; + } + + return EditorQtApplication::eventFilter(object, event); + } +} // namespace Editor diff --git a/Code/Editor/Platform/Windows/Editor/Core/QtEditorApplication_windows.h b/Code/Editor/Platform/Windows/Editor/Core/QtEditorApplication_windows.h new file mode 100644 index 0000000000..6967d5a3b0 --- /dev/null +++ b/Code/Editor/Platform/Windows/Editor/Core/QtEditorApplication_windows.h @@ -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 + * + */ + +#include + +namespace Editor +{ + class EditorQtApplicationWindows : public EditorQtApplication + { + Q_OBJECT + public: + EditorQtApplicationWindows(int& argc, char** argv) + : EditorQtApplication(argc, argv) + { + } + + // QAbstractNativeEventFilter: + bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override; + + bool eventFilter(QObject* object, QEvent* event) override; + }; +} // namespace Editor diff --git a/Code/Editor/Platform/Windows/platform_windows_files.cmake b/Code/Editor/Platform/Windows/platform_windows_files.cmake index d7fc96e0a3..df7a3c817f 100644 --- a/Code/Editor/Platform/Windows/platform_windows_files.cmake +++ b/Code/Editor/Platform/Windows/platform_windows_files.cmake @@ -7,5 +7,6 @@ # set(FILES + Editor/Core/QtEditorApplication_windows.cpp Util/Mailer_Windows.cpp ) diff --git a/Code/Editor/Plugins/EditorCommon/SaveUtilities/AsyncSaveRunner.cpp b/Code/Editor/Plugins/EditorCommon/SaveUtilities/AsyncSaveRunner.cpp index 0b514513ca..d05074c483 100644 --- a/Code/Editor/Plugins/EditorCommon/SaveUtilities/AsyncSaveRunner.cpp +++ b/Code/Editor/Plugins/EditorCommon/SaveUtilities/AsyncSaveRunner.cpp @@ -76,6 +76,7 @@ namespace AZ else if (info.m_status == AzToolsFramework::SourceControlStatus::SCS_ProviderIsDown) { message = "Failed to put entries/dependencies into source control as the provider is not available.\n"; + reportAsWarning = true; } else if (info.m_status == AzToolsFramework::SourceControlStatus::SCS_CertificateInvalid) { diff --git a/Code/Editor/StartupLogoDialog.cpp b/Code/Editor/StartupLogoDialog.cpp index ab251b1564..135c76cf7f 100644 --- a/Code/Editor/StartupLogoDialog.cpp +++ b/Code/Editor/StartupLogoDialog.cpp @@ -34,11 +34,12 @@ CStartupLogoDialog::CStartupLogoDialog(QString versionText, QString richTextCopy m_ui->setupUi(this); s_pLogoWindow = this; - setFixedSize(QSize(600, 300)); + setFixedSize(QSize(m_enforcedWidth, m_enforcedHeight)); + setAttribute(Qt::WA_TranslucentBackground, true); // Prepare background image m_backgroundImage = AzQtComponents::ScalePixmapForScreenDpi( - QPixmap(QStringLiteral(":/StartupLogoDialog/splashscreen_background_developer_preview.jpg")), + QPixmap(QStringLiteral(":/StartupLogoDialog/splashscreen_background_2021_11.jpg")), screen(), QSize(m_enforcedWidth, m_enforcedHeight), Qt::IgnoreAspectRatio, diff --git a/Code/Editor/StartupLogoDialog.h b/Code/Editor/StartupLogoDialog.h index af80ab7e90..072ad6a905 100644 --- a/Code/Editor/StartupLogoDialog.h +++ b/Code/Editor/StartupLogoDialog.h @@ -50,7 +50,7 @@ private: QScopedPointer m_ui; QPixmap m_backgroundImage; - const int m_enforcedWidth = 600; - const int m_enforcedHeight = 300; + const int m_enforcedWidth = 668; + const int m_enforcedHeight = 368; }; diff --git a/Code/Editor/StartupLogoDialog.qrc b/Code/Editor/StartupLogoDialog.qrc index 38d1d1da2a..2d7dd81e87 100644 --- a/Code/Editor/StartupLogoDialog.qrc +++ b/Code/Editor/StartupLogoDialog.qrc @@ -1,6 +1,6 @@ o3de_logo.svg - splashscreen_background_developer_preview.jpg + splashscreen_background_2021_11.jpg diff --git a/Code/Editor/StartupLogoDialog.ui b/Code/Editor/StartupLogoDialog.ui index 60969f7f7f..c0b8115cb0 100644 --- a/Code/Editor/StartupLogoDialog.ui +++ b/Code/Editor/StartupLogoDialog.ui @@ -6,13 +6,22 @@ 0 0 - 600 - 300 + 668 + 368 + + 50 + + + 42 + + + 42 + - 9 + 42 10 @@ -29,7 +38,7 @@ - 250 + 300 20 @@ -53,7 +62,7 @@ 1 - 28 + 20 24 @@ -94,7 +103,7 @@ - Developer Preview + General Availability @@ -153,20 +162,28 @@ 290 - 0 + 32 290 - 16777215 + 32 + + + 8 + + Starting Editor... + + Qt::PlainText + - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop diff --git a/Code/Editor/editor_lib_files.cmake b/Code/Editor/editor_lib_files.cmake index 031ad26d76..47b69765ba 100644 --- a/Code/Editor/editor_lib_files.cmake +++ b/Code/Editor/editor_lib_files.cmake @@ -330,8 +330,6 @@ set(FILES Commands/CommandManager.h Controls/BitmapToolTip.cpp Controls/BitmapToolTip.h - Controls/ColorGradientCtrl.cpp - Controls/ColorGradientCtrl.h Controls/ConsoleSCB.cpp Controls/ConsoleSCB.h Controls/ConsoleSCB.ui diff --git a/Code/Editor/splashscreen_background_2021_11.jpg b/Code/Editor/splashscreen_background_2021_11.jpg new file mode 100644 index 0000000000..703884fc53 --- /dev/null +++ b/Code/Editor/splashscreen_background_2021_11.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffcb7614bed0790bf58a2bb7b2d70958289cb2edf562acc8fc841f4c50a55445 +size 2974586 diff --git a/Code/Editor/splashscreen_background_developer_preview.jpg b/Code/Editor/splashscreen_background_developer_preview.jpg deleted file mode 100644 index 59f05a64df..0000000000 --- a/Code/Editor/splashscreen_background_developer_preview.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7105ec99477f124a8ac8d588f2dfc4ee7bb54f39386c8131b7703c86754c0cb8 -size 248690 diff --git a/Code/Framework/AzCore/AzCore/Debug/Trace.cpp b/Code/Framework/AzCore/AzCore/Debug/Trace.cpp index cde8c36a4e..357149d096 100644 --- a/Code/Framework/AzCore/AzCore/Debug/Trace.cpp +++ b/Code/Framework/AzCore/AzCore/Debug/Trace.cpp @@ -577,7 +577,9 @@ namespace AZ } azstrcat(lines[i], AZ_ARRAY_SIZE(lines[i]), "\n"); - AZ_Printf(window, "%s", lines[i]); // feed back into the trace system so that listeners can get it. + // Use Output instead of AZ_Printf to be consistent with the exception output code and avoid + // this accidentally being suppressed as a normal message + Output(window, lines[i]); } } } diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.cpp new file mode 100644 index 0000000000..2c856fb4b0 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.cpp @@ -0,0 +1,254 @@ +/* + * 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 AZ +{ + JsonSerializationResult::ResultCode JsonImportResolver::ResolveNestedImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack, + JsonImportSettings& settings, const AZ::IO::FixedMaxPath& importPath, StackedString& element) + { + using namespace JsonSerializationResult; + + for (auto& path : importPathStack) + { + if (importPath == path) + { + return settings.m_reporting( + AZStd::string::format("'%s' was already imported in this chain. This indicates a cyclic dependency.", importPath.c_str()), + ResultCode(Tasks::Import, Outcomes::Catastrophic), element); + } + } + + importPathStack.push_back(importPath); + AZ::StackedString importElement(AZ::StackedString::Format::JsonPointer); + JsonImportSettings nestedImportSettings; + nestedImportSettings.m_importer = settings.m_importer; + nestedImportSettings.m_reporting = settings.m_reporting; + nestedImportSettings.m_resolveFlags = ImportTracking::Dependencies; + ResultCode result = ResolveImports(jsonDoc, allocator, importPathStack, nestedImportSettings, importElement); + importPathStack.pop_back(); + + if (result.GetOutcome() == Outcomes::Catastrophic) + { + return result; + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode JsonImportResolver::ResolveImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack, + JsonImportSettings& settings, StackedString& element) + { + using namespace JsonSerializationResult; + + if (jsonDoc.IsObject()) + { + for (auto& field : jsonDoc.GetObject()) + { + if(strncmp(field.name.GetString(), JsonSerialization::ImportDirectiveIdentifier, field.name.GetStringLength()) == 0) + { + const rapidjson::Value& importDirective = field.value; + AZ::IO::FixedMaxPath importAbsPath = importPathStack.back(); + importAbsPath.RemoveFilename(); + AZStd::string importName; + if (importDirective.IsObject()) + { + auto filenameField = importDirective.FindMember("filename"); + if (filenameField != importDirective.MemberEnd()) + { + importName = AZStd::string(filenameField->value.GetString(), filenameField->value.GetStringLength()); + } + } + else + { + importName = AZStd::string(importDirective.GetString(), importDirective.GetStringLength()); + } + importAbsPath.Append(importName); + + rapidjson::Value patch; + ResultCode resolveResult = settings.m_importer->ResolveImport(&jsonDoc, patch, importDirective, importAbsPath, allocator); + if (resolveResult.GetOutcome() == Outcomes::Catastrophic) + { + return resolveResult; + } + + if ((settings.m_resolveFlags & ImportTracking::Imports) == ImportTracking::Imports) + { + rapidjson::Pointer path(element.Get().data(), element.Get().size()); + settings.m_importer->AddImportDirective(path, importName); + } + if ((settings.m_resolveFlags & ImportTracking::Dependencies) == ImportTracking::Dependencies) + { + settings.m_importer->AddImportedFile(importAbsPath.String()); + } + + ResultCode result = ResolveNestedImports(jsonDoc, allocator, importPathStack, settings, importAbsPath, element); + if (result.GetOutcome() == Outcomes::Catastrophic) + { + return result; + } + settings.m_importer->ApplyPatch(jsonDoc, patch, allocator); + } + else if (field.value.IsObject() || field.value.IsArray()) + { + ScopedStackedString entryName(element, AZStd::string_view(field.name.GetString(), field.name.GetStringLength())); + ResultCode result = ResolveImports(field.value, allocator, importPathStack, settings, element); + if (result.GetOutcome() == Outcomes::Catastrophic) + { + return result; + } + } + } + } + else if(jsonDoc.IsArray()) + { + int index = 0; + for (rapidjson::Value::ValueIterator elem = jsonDoc.Begin(); elem != jsonDoc.End(); ++elem, ++index) + { + if (!elem->IsObject() && !elem->IsArray()) + { + continue; + } + ScopedStackedString entryName(element, index); + ResultCode result = ResolveImports(*elem, allocator, importPathStack, settings, element); + if (result.GetOutcome() == Outcomes::Catastrophic) + { + return result; + } + } + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode JsonImportResolver::RestoreImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings) + { + using namespace JsonSerializationResult; + + if (jsonDoc.IsObject() || jsonDoc.IsArray()) + { + const BaseJsonImporter::ImportDirectivesList& importDirectives = settings.m_importer->GetImportDirectives(); + for (auto& import : importDirectives) + { + rapidjson::Pointer importPtr = import.first; + rapidjson::Value* currentValue = importPtr.Get(jsonDoc); + + rapidjson::Value importedValue(rapidjson::kObjectType); + importedValue.AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), rapidjson::StringRef(import.second.c_str()), allocator); + ResultCode resolveResult = JsonSerialization::ResolveImports(importedValue, allocator, settings); + if (resolveResult.GetOutcome() == Outcomes::Catastrophic) + { + return resolveResult; + } + + rapidjson::Value patch; + settings.m_importer->CreatePatch(patch, importedValue, *currentValue, allocator); + settings.m_importer->RestoreImport(currentValue, patch, allocator, import.second); + } + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode BaseJsonImporter::ResolveImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, const rapidjson::Value& importDirective, + const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator) + { + using namespace JsonSerializationResult; + + auto importedObject = JsonSerializationUtils::ReadJsonFile(importedFilePath.Native()); + if (importedObject.IsSuccess()) + { + rapidjson::Value& importedDoc = importedObject.GetValue(); + + if (importDirective.IsObject()) + { + auto patchField = importDirective.FindMember("patch"); + if (patchField != importDirective.MemberEnd()) + { + patch.CopyFrom(patchField->value, allocator); + } + } + + importPtr->CopyFrom(importedDoc, allocator); + } + else + { + return ResultCode(Tasks::Import, Outcomes::Catastrophic); + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode BaseJsonImporter::RestoreImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator, const AZStd::string& importFilename) + { + using namespace JsonSerializationResult; + + importPtr->SetObject(); + if ((patch.IsObject() && patch.MemberCount() > 0) || (patch.IsArray() && !patch.Empty())) + { + rapidjson::Value importDirective(rapidjson::kObjectType); + importDirective.AddMember(rapidjson::StringRef("filename"), rapidjson::StringRef(importFilename.c_str()), allocator); + importDirective.AddMember(rapidjson::StringRef("patch"), patch, allocator); + importPtr->AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), importDirective, allocator); + } + else + { + importPtr->AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), rapidjson::StringRef(importFilename.c_str()), allocator); + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode BaseJsonImporter::ApplyPatch(rapidjson::Value& target, + const rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator) + { + using namespace JsonSerializationResult; + + if ((patch.IsObject() && patch.MemberCount() > 0) || (patch.IsArray() && !patch.Empty())) + { + return AZ::JsonSerialization::ApplyPatch(target, allocator, patch, JsonMergeApproach::JsonMergePatch); + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode BaseJsonImporter::CreatePatch(rapidjson::Value& patch, + const rapidjson::Value& source, const rapidjson::Value& target, + rapidjson::Document::AllocatorType& allocator) + { + return JsonSerialization::CreatePatch(patch, allocator, source, target, JsonMergeApproach::JsonMergePatch); + } + + void BaseJsonImporter::AddImportDirective(const rapidjson::Pointer& jsonPtr, AZStd::string importFile) + { + m_importDirectives.emplace_back(jsonPtr, AZStd::move(importFile)); + } + + void BaseJsonImporter::AddImportedFile(AZStd::string importedFile) + { + m_importedFiles.insert(AZStd::move(importedFile)); + } + + const BaseJsonImporter::ImportDirectivesList& BaseJsonImporter::GetImportDirectives() + { + return m_importDirectives; + } + + const BaseJsonImporter::ImportedFilesList& BaseJsonImporter::GetImportedFiles() + { + return m_importedFiles; + } +} // namespace AZ diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.h b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.h new file mode 100644 index 0000000000..ef53e265d8 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.h @@ -0,0 +1,108 @@ +/* + * 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 +#include +#include +#include + +namespace AZ +{ + struct JsonImportSettings; + + class BaseJsonImporter + { + public: + AZ_RTTI(BaseJsonImporter, "{7B225807-7B43-430F-8B11-C794DCF5ACA5}"); + + using ImportDirectivesList = AZStd::vector>; + using ImportedFilesList = AZStd::unordered_set; + + virtual JsonSerializationResult::ResultCode ResolveImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, const rapidjson::Value& importDirective, + const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator); + + virtual JsonSerializationResult::ResultCode RestoreImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator, + const AZStd::string& importFilename); + + virtual JsonSerializationResult::ResultCode ApplyPatch(rapidjson::Value& target, + const rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator); + + virtual JsonSerializationResult::ResultCode CreatePatch(rapidjson::Value& patch, + const rapidjson::Value& source, const rapidjson::Value& target, + rapidjson::Document::AllocatorType& allocator); + + void AddImportDirective(const rapidjson::Pointer& jsonPtr, AZStd::string importFile); + const ImportDirectivesList& GetImportDirectives(); + + void AddImportedFile(AZStd::string importedFile); + const ImportedFilesList& GetImportedFiles(); + + virtual ~BaseJsonImporter() = default; + + protected: + + ImportDirectivesList m_importDirectives; + ImportedFilesList m_importedFiles; + }; + + enum class ImportTracking : AZ::u8 + { + None = 0, + Dependencies = (1<<0), + Imports = (1<<1), + All = (Dependencies | Imports) + }; + AZ_DEFINE_ENUM_BITWISE_OPERATORS(ImportTracking); + + class JsonImportResolver final + { + public: + + using ImportPathStack = AZStd::vector; + + JsonImportResolver() = delete; + JsonImportResolver& operator=(const JsonImportResolver& rhs) = delete; + JsonImportResolver& operator=(JsonImportResolver&& rhs) = delete; + JsonImportResolver(const JsonImportResolver& rhs) = delete; + JsonImportResolver(JsonImportResolver&& rhs) = delete; + ~JsonImportResolver() = delete; + + static JsonSerializationResult::ResultCode ResolveImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack, + JsonImportSettings& settings, StackedString& element); + + static JsonSerializationResult::ResultCode RestoreImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings); + + private: + + static JsonSerializationResult::ResultCode ResolveNestedImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack, + JsonImportSettings& settings, const AZ::IO::FixedMaxPath& importPath, StackedString& element); + }; + + + struct JsonImportSettings final + { + JsonSerializationResult::JsonIssueCallback m_reporting; + + BaseJsonImporter* m_importer = nullptr; + + ImportTracking m_resolveFlags = ImportTracking::All; + + AZ::IO::FixedMaxPath m_loadedJsonPath; + }; +} // namespace AZ diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonMerger.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonMerger.cpp index 45549b8078..a9b2d2fefa 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonMerger.cpp +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonMerger.cpp @@ -706,7 +706,7 @@ namespace AZ rapidjson::Value(rapidjson::kNullType), field.value, element, settings); } - if (result.GetOutcome() == Outcomes::Success) + if (result.GetOutcome() == Outcomes::Success || result.GetOutcome() == Outcomes::PartialDefaults) { rapidjson::Value name; name.CopyFrom(field.name, allocator, true); @@ -717,6 +717,10 @@ namespace AZ { return result; } + else + { + resultCode.Combine(result); + } } // Do an extra pass to find all the fields that are removed. @@ -751,7 +755,7 @@ namespace AZ rapidjson::Value value; ResultCode result = CreateMergePatchInternal(value, allocator, rapidjson::Value(rapidjson::kNullType), field.value, element, settings); - if (result.GetOutcome() == Outcomes::Success) + if (result.GetOutcome() == Outcomes::Success || result.GetOutcome() == Outcomes::PartialDefaults) { rapidjson::Value name; name.CopyFrom(field.name, allocator, true); @@ -762,11 +766,20 @@ namespace AZ { return result; } + else + { + resultCode.Combine(result); + } + } + + if (target.MemberCount() == 0) + { + resultCode.Combine(settings.m_reporting("Added empty object to JSON Merge Patch.", + ResultCode(Tasks::CreatePatch, Outcomes::Success), element)); } } patch = AZStd::move(resultValue); - resultCode.Combine(ResultCode(Tasks::CreatePatch, Outcomes::Success)); return resultCode; } else diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.cpp index bc07f684f6..db76e46f2b 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.cpp +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -19,11 +20,6 @@ namespace AZ { - const char* JsonSerialization::TypeIdFieldIdentifier = "$type"; - const char* JsonSerialization::DefaultStringIdentifier = "{}"; - const char* JsonSerialization::KeyFieldIdentifier = "Key"; - const char* JsonSerialization::ValueFieldIdentifier = "Value"; - namespace JsonSerializationInternal { template @@ -394,6 +390,60 @@ namespace AZ } } + JsonSerializationResult::ResultCode JsonSerialization::ResolveImports( + rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings) + { + using namespace JsonSerializationResult; + + if (settings.m_importer == nullptr) + { + AZ_Assert(false, "Importer object needs to be provided"); + return ResultCode(Tasks::Import, Outcomes::Catastrophic); + } + + AZStd::string scratchBuffer; + auto issueReportingCallback = [&scratchBuffer](AZStd::string_view message, ResultCode result, AZStd::string_view target) -> ResultCode + { + return JsonSerialization::DefaultIssueReporter(scratchBuffer, message, result, target); + }; + if (!settings.m_reporting) + { + settings.m_reporting = issueReportingCallback; + } + + JsonImportResolver::ImportPathStack importPathStack; + importPathStack.push_back(settings.m_loadedJsonPath); + StackedString element(StackedString::Format::JsonPointer); + + return JsonImportResolver::ResolveImports(jsonDoc, allocator, importPathStack, settings, element); + } + + JsonSerializationResult::ResultCode JsonSerialization::RestoreImports( + rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings) + { + using namespace JsonSerializationResult; + + if (settings.m_importer == nullptr) + { + AZ_Assert(false, "Importer object needs to be provided"); + return ResultCode(Tasks::Import, Outcomes::Catastrophic); + } + + AZStd::string scratchBuffer; + auto issueReportingCallback = [&scratchBuffer](AZStd::string_view message, ResultCode result, AZStd::string_view target) -> ResultCode + { + return JsonSerialization::DefaultIssueReporter(scratchBuffer, message, result, target); + }; + if (!settings.m_reporting) + { + settings.m_reporting = issueReportingCallback; + } + + settings.m_resolveFlags = ImportTracking::None; + + return JsonImportResolver::RestoreImports(jsonDoc, allocator, settings); + } + JsonSerializationResult::ResultCode JsonSerialization::DefaultIssueReporter(AZStd::string& scratchBuffer, AZStd::string_view message, JsonSerializationResult::ResultCode result, AZStd::string_view path) { diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.h b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.h index c85847ac78..d961953a1d 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.h +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.h @@ -18,6 +18,8 @@ namespace AZ { class BaseJsonSerializer; + + struct JsonImportSettings; enum class JsonMergeApproach { @@ -51,10 +53,11 @@ namespace AZ class JsonSerialization final { public: - static const char* TypeIdFieldIdentifier; - static const char* DefaultStringIdentifier; - static const char* KeyFieldIdentifier; - static const char* ValueFieldIdentifier; + static constexpr const char* TypeIdFieldIdentifier = "$type"; + static constexpr const char* DefaultStringIdentifier = "{}"; + static constexpr const char* KeyFieldIdentifier = "Key"; + static constexpr const char* ValueFieldIdentifier = "Value"; + static constexpr const char* ImportDirectiveIdentifier = "$import"; //! Merges two json values together by applying "patch" to "target" using the selected merge algorithm. //! This version of ApplyPatch is destructive to "target". If the patch can't be correctly applied it will @@ -284,6 +287,22 @@ namespace AZ //! @return An enum containing less, equal or greater. In case of an error, the value for the enum will "error". static JsonSerializerCompareResult Compare(const rapidjson::Value& lhs, const rapidjson::Value& rhs); + //! Resolves all import directives, including nested imports, in the given document. An importer object needs to be passed + //! in through the settings. + //! @param jsonDoc The json document in which to resolve imports. + //! @param allocator The allocator associated with the json document. + //! @param settings Additional settings that control the way the imports are resolved. + static JsonSerializationResult::ResultCode ResolveImports( + rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings); + + //! Restores all import directives that were present in the json document. The same importer object that was + //! passed into ResolveImports through the settings needs to be passed here through settings as well. + //! @param jsonDoc The json document in which to restore imports. + //! @param allocator The allocator associated with the json document. + //! @param settings Additional settings that control the way the imports are restored. + static JsonSerializationResult::ResultCode RestoreImports( + rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings); + private: JsonSerialization() = delete; ~JsonSerialization() = delete; diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.cpp index 7e84aced7b..822c1c43d5 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.cpp +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.cpp @@ -69,6 +69,9 @@ namespace AZ case Tasks::CreatePatch: target.append("a create patch operation "); break; + case Tasks::Import: + target.append("an import operation"); + break; default: target.append("an unknown operation "); break; diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.h b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.h index 8590971a1c..204c40b8ca 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.h +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.h @@ -32,7 +32,8 @@ namespace AZ ReadField, //!< Task to read a field from JSON to a value. WriteValue, //!< Task to write a value to a JSON field. Merge, //!< Task to merge two JSON values/documents together. - CreatePatch //!< Task to create a patch to transform one value/document to another. + CreatePatch, //!< Task to create a patch to transform one value/document to another. + Import //!< Task to import a JSON document. }; //! Describes how the task was processed. diff --git a/Code/Framework/AzCore/AzCore/Utils/Utils.cpp b/Code/Framework/AzCore/AzCore/Utils/Utils.cpp index 2031d14d08..e6bfd78806 100644 --- a/Code/Framework/AzCore/AzCore/Utils/Utils.cpp +++ b/Code/Framework/AzCore/AzCore/Utils/Utils.cpp @@ -120,7 +120,7 @@ namespace AZ::Utils AZ::Outcome WriteFile(AZStd::string_view content, AZStd::string_view filePath) { AZ::IO::FixedMaxPath filePathFixed = filePath; // Because FileIOStream requires a null-terminated string - AZ::IO::FileIOStream stream(filePathFixed.c_str(), AZ::IO::OpenMode::ModeWrite); + AZ::IO::FileIOStream stream(filePathFixed.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath); bool success = false; diff --git a/Code/Framework/AzCore/AzCore/azcore_files.cmake b/Code/Framework/AzCore/AzCore/azcore_files.cmake index 4d95ddf098..41229429f2 100644 --- a/Code/Framework/AzCore/AzCore/azcore_files.cmake +++ b/Code/Framework/AzCore/AzCore/azcore_files.cmake @@ -522,6 +522,8 @@ set(FILES Serialization/Json/IntSerializer.cpp Serialization/Json/JsonDeserializer.h Serialization/Json/JsonDeserializer.cpp + Serialization/Json/JsonImporter.cpp + Serialization/Json/JsonImporter.h Serialization/Json/JsonMerger.h Serialization/Json/JsonMerger.cpp Serialization/Json/JsonSerialization.h diff --git a/Code/Framework/AzCore/Tests/Serialization/Json/TestCases_Importing.cpp b/Code/Framework/AzCore/Tests/Serialization/Json/TestCases_Importing.cpp new file mode 100644 index 0000000000..5121efa5e3 --- /dev/null +++ b/Code/Framework/AzCore/Tests/Serialization/Json/TestCases_Importing.cpp @@ -0,0 +1,413 @@ +/* + * 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 JsonSerializationTests +{ + class JsonImportingTests; + + class JsonImporterCustom + : public AZ::BaseJsonImporter + { + public: + AZ_RTTI(JsonImporterCustom, "{003F5896-71E0-4A50-A14F-08C319B06AD0}"); + + + AZ::JsonSerializationResult::ResultCode ResolveImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, const rapidjson::Value& importDirective, + const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator) override; + + JsonImporterCustom(JsonImportingTests* tests) + { + testClass = tests; + } + + private: + JsonImportingTests* testClass; + }; + + class JsonImportingTests + : public BaseJsonSerializerFixture + { + public: + void SetUp() override + { + BaseJsonSerializerFixture::SetUp(); + } + + void TearDown() override + { + BaseJsonSerializerFixture::TearDown(); + } + + void GetTestDocument(const AZStd::string& docName, rapidjson::Document& out) + { + const char *objectJson = R"({ + "field_1" : "value_1", + "field_2" : "value_2", + "field_3" : "value_3" + })"; + + const char *arrayJson = R"([ + { "element_1" : "value_1" }, + { "element_2" : "value_2" }, + { "element_3" : "value_3" } + ])"; + + const char *nestedImportJson = R"({ + "desc" : "Nested Import", + "obj" : {"$import" : "object.json"} + })"; + + const char *nestedImportCycle1Json = R"({ + "desc" : "Nested Import Cycle 1", + "obj" : {"$import" : "nested_import_c2.json"} + })"; + + const char *nestedImportCycle2Json = R"({ + "desc" : "Nested Import Cycle 2", + "obj" : {"$import" : "nested_import_c1.json"} + })"; + + if (docName.compare("object.json") == 0) + { + out.Parse(objectJson); + ASSERT_FALSE(out.HasParseError()); + } + else if (docName.compare("array.json") == 0) + { + out.Parse(arrayJson); + ASSERT_FALSE(out.HasParseError()); + } + else if (docName.compare("nested_import.json") == 0) + { + out.Parse(nestedImportJson); + ASSERT_FALSE(out.HasParseError()); + } + else if (docName.compare("nested_import_c1.json") == 0) + { + out.Parse(nestedImportCycle1Json); + ASSERT_FALSE(out.HasParseError()); + } + else if (docName.compare("nested_import_c2.json") == 0) + { + out.Parse(nestedImportCycle2Json); + ASSERT_FALSE(out.HasParseError()); + } + } + + protected: + void TestImportLoadStore(const char* input, const char* expectedImportedValue) + { + m_jsonDocument->Parse(input); + ASSERT_FALSE(m_jsonDocument->HasParseError()); + + JsonImporterCustom* importerObj = new JsonImporterCustom(this); + + rapidjson::Document expectedOutcome; + expectedOutcome.Parse(expectedImportedValue); + ASSERT_FALSE(expectedOutcome.HasParseError()); + + TestResolveImports(importerObj); + + Expect_DocStrEq(m_jsonDocument->GetObject(), expectedOutcome.GetObject()); + + rapidjson::Document originalInput; + originalInput.Parse(input); + ASSERT_FALSE(originalInput.HasParseError()); + + TestRestoreImports(importerObj); + + Expect_DocStrEq(m_jsonDocument->GetObject(), originalInput.GetObject()); + + m_jsonDocument->SetObject(); + delete importerObj; + } + + void TestImportCycle(const char* input) + { + m_jsonDocument->Parse(input); + ASSERT_FALSE(m_jsonDocument->HasParseError()); + + JsonImporterCustom* importerObj = new JsonImporterCustom(this); + + AZ::JsonSerializationResult::ResultCode result = TestResolveImports(importerObj); + + EXPECT_EQ(result.GetOutcome(), AZ::JsonSerializationResult::Outcomes::Catastrophic); + + m_jsonDocument->SetObject(); + delete importerObj; + } + + void TestInsertNewImport(const char* input, const char* expectedRestoredValue) + { + m_jsonDocument->Parse(input); + ASSERT_FALSE(m_jsonDocument->HasParseError()); + + JsonImporterCustom* importerObj = new JsonImporterCustom(this); + + TestResolveImports(importerObj); + + importerObj->AddImportDirective(rapidjson::Pointer("/object_2"), "object.json"); + + rapidjson::Document expectedOutput; + expectedOutput.Parse(expectedRestoredValue); + ASSERT_FALSE(expectedOutput.HasParseError()); + + TestRestoreImports(importerObj); + + Expect_DocStrEq(m_jsonDocument->GetObject(), expectedOutput.GetObject()); + + m_jsonDocument->SetObject(); + delete importerObj; + } + + AZ::JsonSerializationResult::ResultCode TestResolveImports(JsonImporterCustom* importerObj) + { + AZ::JsonImportSettings settings; + settings.m_importer = importerObj; + + return AZ::JsonSerialization::ResolveImports(m_jsonDocument->GetObject(), m_jsonDocument->GetAllocator(), settings); + } + + AZ::JsonSerializationResult::ResultCode TestRestoreImports(JsonImporterCustom* importerObj) + { + AZ::JsonImportSettings settings; + settings.m_importer = importerObj; + + return AZ::JsonSerialization::RestoreImports(m_jsonDocument->GetObject(), m_jsonDocument->GetAllocator(), settings); + } + }; + + AZ::JsonSerializationResult::ResultCode JsonImporterCustom::ResolveImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, const rapidjson::Value& importDirective, const AZ::IO::FixedMaxPath& importedFilePath, + rapidjson::Document::AllocatorType& allocator) + { + AZ::JsonSerializationResult::ResultCode resultCode(AZ::JsonSerializationResult::Tasks::Import); + + rapidjson::Document importedDoc; + testClass->GetTestDocument(importedFilePath.String(), importedDoc); + + if (importDirective.IsObject()) + { + auto patchField = importDirective.FindMember("patch"); + if (patchField != importDirective.MemberEnd()) + { + patch.CopyFrom(patchField->value, allocator); + } + } + + importPtr->CopyFrom(importedDoc, allocator); + + return resultCode; + } + + // Test Cases + + TEST_F(JsonImportingTests, ImportSimpleObjectTest) + { + const char* inputFile = R"( + { + "name" : "simple_object_import", + "object": {"$import" : "object.json"} + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_object_import", + "object": { + "field_1" : "value_1", + "field_2" : "value_2", + "field_3" : "value_3" + } + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, ImportSimpleObjectPatchTest) + { + const char* inputFile = R"( + { + "name" : "simple_object_import", + "object": { + "$import" : { + "filename" : "object.json", + "patch" : { "field_2" : "patched_value" } + } + } + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_object_import", + "object": { + "field_1" : "value_1", + "field_2" : "patched_value", + "field_3" : "value_3" + } + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, ImportSimpleArrayTest) + { + const char* inputFile = R"( + { + "name" : "simple_array_import", + "object": {"$import" : "array.json"} + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_array_import", + "object": [ + { "element_1" : "value_1" }, + { "element_2" : "value_2" }, + { "element_3" : "value_3" } + ] + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, ImportSimpleArrayPatchTest) + { + const char* inputFile = R"( + { + "name" : "simple_array_import", + "object": { + "$import" : { + "filename" : "array.json", + "patch" : [ { "element_1" : "patched_value" } ] + } + } + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_array_import", + "object": [ + { "element_1" : "patched_value" } + ] + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, NestedImportTest) + { + const char* inputFile = R"( + { + "name" : "nested_import", + "object": {"$import" : "nested_import.json"} + } + )"; + + const char* expectedOutput = R"( + { + "name" : "nested_import", + "object": { + "desc" : "Nested Import", + "obj" : { + "field_1" : "value_1", + "field_2" : "value_2", + "field_3" : "value_3" + } + } + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, NestedImportPatchTest) + { + const char* inputFile = R"( + { + "name" : "nested_import", + "object": { + "$import" : { + "filename" : "nested_import.json", + "patch" : { "obj" : { "field_3" : "patched_value" } } + } + } + } + )"; + + const char* expectedOutput = R"( + { + "name" : "nested_import", + "object": { + "desc" : "Nested Import", + "obj" : { + "field_1" : "value_1", + "field_2" : "value_2", + "field_3" : "patched_value" + } + } + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, NestedImportCycleTest) + { + const char* inputFile = R"( + { + "name" : "nested_import_cycle", + "object": {"$import" : "nested_import_c1.json"} + } + )"; + + TestImportCycle(inputFile); + } + + TEST_F(JsonImportingTests, InsertNewImportTest) + { + const char* inputFile = R"( + { + "name" : "simple_object_import", + "object_1": {"$import" : "object.json"}, + "object_2": { + "field_1" : "other_value", + "field_2" : "value_2", + "field_3" : "value_3" + } + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_object_import", + "object_1": {"$import" : "object.json"}, + "object_2": { + "$import" : { + "filename" : "object.json", + "patch" : { "field_1" : "other_value" } + } + } + } + )"; + + TestInsertNewImport(inputFile, expectedOutput); + } +} diff --git a/Code/Framework/AzCore/Tests/azcoretests_files.cmake b/Code/Framework/AzCore/Tests/azcoretests_files.cmake index cc6000209f..d39595c45e 100644 --- a/Code/Framework/AzCore/Tests/azcoretests_files.cmake +++ b/Code/Framework/AzCore/Tests/azcoretests_files.cmake @@ -121,6 +121,7 @@ set(FILES Serialization/Json/TestCases_Classes.cpp Serialization/Json/TestCases_Compare.cpp Serialization/Json/TestCases_Enum.cpp + Serialization/Json/TestCases_Importing.cpp Serialization/Json/TestCases_Patching.cpp Serialization/Json/TestCases_Pointers.h Serialization/Json/TestCases_Pointers.cpp diff --git a/Code/Framework/AzFramework/AzFramework/Application/Application.cpp b/Code/Framework/AzFramework/AzFramework/Application/Application.cpp index 1f99a594fa..f8d0ea8bb1 100644 --- a/Code/Framework/AzFramework/AzFramework/Application/Application.cpp +++ b/Code/Framework/AzFramework/AzFramework/Application/Application.cpp @@ -58,6 +58,7 @@ #include #include #include +#include #include #include #include @@ -278,6 +279,8 @@ namespace AzFramework AzFramework::RemoteStorageDriveConfig::Reflect(context); Physics::ReflectionUtils::ReflectPhysicsApi(context); + AzFramework::SurfaceData::SurfaceTagWeight::Reflect(context); + AzFramework::SurfaceData::SurfacePoint::Reflect(context); AzFramework::Terrain::TerrainDataRequests::Reflect(context); if (AZ::SerializeContext* serializeContext = azrtti_cast(context)) diff --git a/Code/Framework/AzFramework/AzFramework/Matchmaking/IMatchmakingRequests.h b/Code/Framework/AzFramework/AzFramework/Matchmaking/IMatchmakingRequests.h index 22b65f8340..c657e0edc7 100644 --- a/Code/Framework/AzFramework/AzFramework/Matchmaking/IMatchmakingRequests.h +++ b/Code/Framework/AzFramework/AzFramework/Matchmaking/IMatchmakingRequests.h @@ -43,7 +43,7 @@ namespace AzFramework class IMatchmakingAsyncRequests { public: - AZ_RTTI(ISessionAsyncRequests, "{53513480-2D02-493C-B44E-96AA27F42429}"); + AZ_RTTI(IMatchmakingAsyncRequests, "{53513480-2D02-493C-B44E-96AA27F42429}"); IMatchmakingAsyncRequests() = default; virtual ~IMatchmakingAsyncRequests() = default; @@ -60,4 +60,31 @@ namespace AzFramework // @param stopMatchmakingRequest The request of StopMatchmaking operation virtual void StopMatchmakingAsync(const StopMatchmakingRequest& stopMatchmakingRequest) = 0; }; + + //! MatchmakingAsyncRequestNotifications + //! The notifications correspond to matchmaking async requests + class MatchmakingAsyncRequestNotifications + : public AZ::EBusTraits + { + public: + // Safeguard handler for multi-threaded use case + using MutexType = AZStd::recursive_mutex; + + ////////////////////////////////////////////////////////////////////////// + // EBusTraits overrides + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + ////////////////////////////////////////////////////////////////////////// + + // OnAcceptMatchAsyncComplete is fired once AcceptMatchAsync completes + virtual void OnAcceptMatchAsyncComplete() = 0; + + // OnStartMatchmakingAsyncComplete is fired once StartMatchmakingAsync completes + // @param matchmakingTicketId The unique identifier for the matchmaking ticket + virtual void OnStartMatchmakingAsyncComplete(const AZStd::string& matchmakingTicketId) = 0; + + // OnStopMatchmakingAsyncComplete is fired once StopMatchmakingAsync completes + virtual void OnStopMatchmakingAsyncComplete() = 0; + }; + using MatchmakingAsyncRequestNotificationBus = AZ::EBus; } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Matchmaking/MatchmakingNotifications.h b/Code/Framework/AzFramework/AzFramework/Matchmaking/MatchmakingNotifications.h index ad61971a11..aa19b94b4a 100644 --- a/Code/Framework/AzFramework/AzFramework/Matchmaking/MatchmakingNotifications.h +++ b/Code/Framework/AzFramework/AzFramework/Matchmaking/MatchmakingNotifications.h @@ -13,36 +13,10 @@ namespace AzFramework { - //! MatchmakingAsyncRequestNotifications - //! The notifications correspond to matchmaking async requests - class MatchmakingAsyncRequestNotifications - : public AZ::EBusTraits - { - public: - // Safeguard handler for multi-threaded use case - using MutexType = AZStd::recursive_mutex; - - ////////////////////////////////////////////////////////////////////////// - // EBusTraits overrides - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - ////////////////////////////////////////////////////////////////////////// - - // OnAcceptMatchAsyncComplete is fired once AcceptMatchAsync completes - virtual void OnAcceptMatchAsyncComplete() = 0; - - // OnStartMatchmakingAsyncComplete is fired once StartMatchmakingAsync completes - // @param matchmakingTicketId The unique identifier for the matchmaking ticket - virtual void OnStartMatchmakingAsyncComplete(const AZStd::string& matchmakingTicketId) = 0; - - // OnStopMatchmakingAsyncComplete is fired once StopMatchmakingAsync completes - virtual void OnStopMatchmakingAsyncComplete() = 0; - }; - using MatchmakingAsyncRequestNotificationBus = AZ::EBus; - //! MatchmakingNotifications //! The matchmaking notifications to listen for performing required operations - class MatchAcceptanceNotifications + //! based on matchmaking ticket event + class MatchmakingNotifications : public AZ::EBusTraits { public: @@ -55,8 +29,18 @@ namespace AzFramework static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; ////////////////////////////////////////////////////////////////////////// - // OnMatchAcceptance is fired when DescribeMatchmaking ticket status is REQUIRES_ACCEPTANCE + // OnMatchAcceptance is fired when match is found and pending on acceptance + // Use this notification to accept found match virtual void OnMatchAcceptance() = 0; + + // OnMatchComplete is fired when match is complete + virtual void OnMatchComplete() = 0; + + // OnMatchError is fired when match is processed with error + virtual void OnMatchError() = 0; + + // OnMatchFailure is fired when match is failed to complete + virtual void OnMatchFailure() = 0; }; - using MatchAcceptanceNotificationBus = AZ::EBus; + using MatchmakingNotificationBus = AZ::EBus; } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Physics/HeightfieldProviderBus.h b/Code/Framework/AzFramework/AzFramework/Physics/HeightfieldProviderBus.h new file mode 100644 index 0000000000..73523ee1ba --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Physics/HeightfieldProviderBus.h @@ -0,0 +1,97 @@ +/* + * 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 Physics +{ + //! The QuadMeshType specifies the property of the heightfield quad. + enum class QuadMeshType : uint8_t + { + SubdivideUpperLeftToBottomRight, //!< Subdivide the quad, from upper left to bottom right |\|, into two triangles. + SubdivideBottomLeftToUpperRight, //!< Subdivide the quad, from bottom left to upper right |/|, into two triangles. + Hole //!< The quad should be treated as a hole in the heightfield. + }; + + struct HeightMaterialPoint + { + float m_height{ 0.0f }; //!< Holds the height of this point in the heightfield relative to the heightfield entity location. + QuadMeshType m_quadMeshType{ QuadMeshType::SubdivideUpperLeftToBottomRight }; //!< By default, create two triangles like this |\|, where this point is in the upper left corner. + uint8_t m_materialIndex{ 0 }; //!< The surface material index for the upper left corner of this quad. + uint16_t m_padding{ 0 }; //!< available for future use. + }; + + //! An interface to provide heightfield values. + class HeightfieldProviderRequests + : public AZ::ComponentBus + { + public: + //! Returns the distance between each height in the map. + //! @return Vector containing Column Spacing, Rows Spacing. + virtual AZ::Vector2 GetHeightfieldGridSpacing() const = 0; + + //! Returns the height field gridsize. + //! @param numColumns contains the size of the grid in the x direction. + //! @param numRows contains the size of the grid in the y direction. + virtual void GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const = 0; + + //! Returns the height field min and max height bounds. + //! @param minHeightBounds contains the minimum height that the heightfield can contain. + //! @param maxHeightBounds contains the maximum height that the heightfield can contain. + virtual void GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const = 0; + + //! Returns the AABB of the heightfield. + //! This is provided separately from the shape AABB because the heightfield might choose to modify the AABB bounds. + //! @return AABB of the heightfield. + virtual AZ::Aabb GetHeightfieldAabb() const = 0; + + //! Returns the world transform for the heightfield. + //! This is provided separately from the entity transform because the heightfield might want to clear out the rotation or scale. + //! @return world transform that should be used with the heightfield data. + virtual AZ::Transform GetHeightfieldTransform() const = 0; + + //! Returns the list of materials used by the height field. + //! @return returns a vector of all materials. + virtual AZStd::vector GetMaterialList() const = 0; + + //! Returns the list of heights used by the height field. + //! @return the rows*columns vector of the heights. + virtual AZStd::vector GetHeights() const = 0; + + //! Returns the list of heights and materials used by the height field. + //! @return the rows*columns vector of the heights and materials. + virtual AZStd::vector GetHeightsAndMaterials() const = 0; + }; + + using HeightfieldProviderRequestsBus = AZ::EBus; + + //! Broadcasts notifications when heightfield data changes - heightfield providers implement HeightfieldRequests bus. + class HeightfieldProviderNotifications + : public AZ::ComponentBus + { + public: + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; + + //! Called whenever the heightfield data changes. + //! @param the AABB of the area of data that changed. + virtual void OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) + { + } + + protected: + ~HeightfieldProviderNotifications() = default; + }; + + using HeightfieldProviderNotificationBus = AZ::EBus; +} // namespace Physics diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Mocks/MockHeightfieldProviderBus.h b/Code/Framework/AzFramework/AzFramework/Physics/Mocks/MockHeightfieldProviderBus.h new file mode 100644 index 0000000000..221c52258d --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Physics/Mocks/MockHeightfieldProviderBus.h @@ -0,0 +1,33 @@ +/* + * 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 + +namespace UnitTest +{ + class MockHeightfieldProviderNotificationBusListener + : private Physics::HeightfieldProviderNotificationBus::Handler + { + public: + MockHeightfieldProviderNotificationBusListener(AZ::EntityId entityid) + { + Physics::HeightfieldProviderNotificationBus::Handler::BusConnect(entityid); + } + + ~MockHeightfieldProviderNotificationBusListener() + { + Physics::HeightfieldProviderNotificationBus::Handler::BusDisconnect(); + } + + MOCK_METHOD1(OnHeightfieldDataChanged, void(const AZ::Aabb&)); + }; +} // namespace UnitTest diff --git a/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.cpp b/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.cpp index e90c9d4eed..db8004d83a 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.cpp @@ -37,6 +37,7 @@ namespace Physics REFLECT_SHAPETYPE_ENUM_VALUE(Sphere); REFLECT_SHAPETYPE_ENUM_VALUE(Cylinder); REFLECT_SHAPETYPE_ENUM_VALUE(PhysicsAsset); + REFLECT_SHAPETYPE_ENUM_VALUE(Heightfield); #undef REFLECT_SHAPETYPE_ENUM_VALUE } @@ -285,12 +286,17 @@ namespace Physics return m_type; } - void* CookedMeshShapeConfiguration::GetCachedNativeMesh() const + const void* CookedMeshShapeConfiguration::GetCachedNativeMesh() const { return m_cachedNativeMesh; } - void CookedMeshShapeConfiguration::SetCachedNativeMesh(void* cachedNativeMesh) const + void* CookedMeshShapeConfiguration::GetCachedNativeMesh() + { + return m_cachedNativeMesh; + } + + void CookedMeshShapeConfiguration::SetCachedNativeMesh(void* cachedNativeMesh) { m_cachedNativeMesh = cachedNativeMesh; } @@ -305,4 +311,131 @@ namespace Physics m_cachedNativeMesh = nullptr; } } -} + + void HeightfieldShapeConfiguration::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext + ->RegisterGenericType>(); + + serializeContext->Class() + ->Version(1); + } + } + + HeightfieldShapeConfiguration::~HeightfieldShapeConfiguration() + { + SetCachedNativeHeightfield(nullptr); + } + + HeightfieldShapeConfiguration::HeightfieldShapeConfiguration(const HeightfieldShapeConfiguration& other) + : ShapeConfiguration(other) + , m_gridResolution(other.m_gridResolution) + , m_numColumns(other.m_numColumns) + , m_numRows(other.m_numRows) + , m_samples(other.m_samples) + , m_minHeightBounds(other.m_minHeightBounds) + , m_maxHeightBounds(other.m_maxHeightBounds) + , m_cachedNativeHeightfield(nullptr) + { + } + + HeightfieldShapeConfiguration& HeightfieldShapeConfiguration::operator=(const HeightfieldShapeConfiguration& other) + { + ShapeConfiguration::operator=(other); + + m_gridResolution = other.m_gridResolution; + m_numColumns = other.m_numColumns; + m_numRows = other.m_numRows; + m_samples = other.m_samples; + m_minHeightBounds = other.m_minHeightBounds; + m_maxHeightBounds = other.m_maxHeightBounds; + + // Prevent raw pointer from being copied + m_cachedNativeHeightfield = nullptr; + + return *this; + } + + const void* HeightfieldShapeConfiguration::GetCachedNativeHeightfield() const + { + return m_cachedNativeHeightfield; + } + + void* HeightfieldShapeConfiguration::GetCachedNativeHeightfield() + { + return m_cachedNativeHeightfield; + } + + void HeightfieldShapeConfiguration::SetCachedNativeHeightfield(void* cachedNativeHeightfield) + { + if (m_cachedNativeHeightfield) + { + Physics::SystemRequestBus::Broadcast(&Physics::SystemRequests::ReleaseNativeHeightfieldObject, m_cachedNativeHeightfield); + } + + m_cachedNativeHeightfield = cachedNativeHeightfield; + } + + AZ::Vector2 HeightfieldShapeConfiguration::GetGridResolution() const + { + return m_gridResolution; + } + + void HeightfieldShapeConfiguration::SetGridResolution(const AZ::Vector2& gridResolution) + { + m_gridResolution = gridResolution; + } + + int32_t HeightfieldShapeConfiguration::GetNumColumns() const + { + return m_numColumns; + } + + void HeightfieldShapeConfiguration::SetNumColumns(int32_t numColumns) + { + m_numColumns = numColumns; + } + + int32_t HeightfieldShapeConfiguration::GetNumRows() const + { + return m_numRows; + } + + void HeightfieldShapeConfiguration::SetNumRows(int32_t numRows) + { + m_numRows = numRows; + } + + const AZStd::vector& HeightfieldShapeConfiguration::GetSamples() const + { + return m_samples; + } + + void HeightfieldShapeConfiguration::SetSamples(const AZStd::vector& samples) + { + m_samples = samples; + } + + float HeightfieldShapeConfiguration::GetMinHeightBounds() const + { + return m_minHeightBounds; + } + + void HeightfieldShapeConfiguration::SetMinHeightBounds(float minBounds) + { + m_minHeightBounds = minBounds; + } + + float HeightfieldShapeConfiguration::GetMaxHeightBounds() const + { + return m_maxHeightBounds; + } + + void HeightfieldShapeConfiguration::SetMaxHeightBounds(float maxBounds) + { + m_maxHeightBounds = maxBounds; + } +} // namespace Physics + diff --git a/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.h b/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.h index b40a8b1edd..bd9d6a6aa7 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.h +++ b/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.h @@ -9,10 +9,13 @@ #pragma once #include +#include #include #include #include +#include + namespace Physics { /// Used to identify shape configuration type from base class. @@ -27,6 +30,7 @@ namespace Physics Native, ///< Native shape configuration if user wishes to bypass generic shape configurations. PhysicsAsset, ///< Shapes configured in the asset. CookedMesh, ///< Stores a blob of mesh data cooked for the specific engine. + Heightfield ///< Interacts with the physics system heightfield }; class ShapeConfiguration @@ -183,8 +187,9 @@ namespace Physics MeshType GetMeshType() const; - void* GetCachedNativeMesh() const; - void SetCachedNativeMesh(void* cachedNativeMesh) const; + void* GetCachedNativeMesh(); + const void* GetCachedNativeMesh() const; + void SetCachedNativeMesh(void* cachedNativeMesh); private: void ReleaseCachedNativeMesh(); @@ -193,7 +198,56 @@ namespace Physics MeshType m_type = MeshType::TriangleMesh; //! Cached native mesh object (e.g. PxConvexMesh or PxTriangleMesh). This data is not serialized. - mutable void* m_cachedNativeMesh = nullptr; + void* m_cachedNativeMesh = nullptr; }; + class HeightfieldShapeConfiguration + : public ShapeConfiguration + { + public: + AZ_CLASS_ALLOCATOR(HeightfieldShapeConfiguration, AZ::SystemAllocator, 0); + AZ_RTTI(HeightfieldShapeConfiguration, "{8DF47C83-D2A9-4E7C-8620-5E173E43C0B3}", ShapeConfiguration); + static void Reflect(AZ::ReflectContext* context); + HeightfieldShapeConfiguration() = default; + HeightfieldShapeConfiguration(const HeightfieldShapeConfiguration&); + HeightfieldShapeConfiguration& operator=(const HeightfieldShapeConfiguration&); + ~HeightfieldShapeConfiguration(); + + ShapeType GetShapeType() const override + { + return ShapeType::Heightfield; + } + + const void* GetCachedNativeHeightfield() const; + void* GetCachedNativeHeightfield(); + void SetCachedNativeHeightfield(void* cachedNativeHeightfield); + AZ::Vector2 GetGridResolution() const; + void SetGridResolution(const AZ::Vector2& gridSpacing); + int32_t GetNumColumns() const; + void SetNumColumns(int32_t numColumns); + int32_t GetNumRows() const; + void SetNumRows(int32_t numRows); + const AZStd::vector& GetSamples() const; + void SetSamples(const AZStd::vector& samples); + float GetMinHeightBounds() const; + void SetMinHeightBounds(float minBounds); + float GetMaxHeightBounds() const; + void SetMaxHeightBounds(float maxBounds); + + private: + //! The number of meters between each heightfield sample. + AZ::Vector2 m_gridResolution{ 1.0f }; + //! The number of columns in the heightfield sample grid. + int32_t m_numColumns{ 0 }; + //! The number of rows in the heightfield sample grid. + int32_t m_numRows{ 0 }; + //! The minimum and maximum heights that can be used by this heightfield. + //! This can be used by the physics system to choose a more optimal heightfield data type internally (ex: int16, uint8) + float m_minHeightBounds{AZStd::numeric_limits::lowest()}; + float m_maxHeightBounds{AZStd::numeric_limits::max()}; + //! The grid of sample points for the heightfield. + AZStd::vector m_samples; + //! An optional storage pointer for the physics system to cache its native heightfield representation. + void* m_cachedNativeHeightfield{ nullptr }; + }; } // namespace Physics diff --git a/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h b/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h index 868a27ed36..33313d612b 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h +++ b/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h @@ -132,6 +132,10 @@ namespace Physics virtual AZStd::shared_ptr CreateMaterial(const Physics::MaterialConfiguration& materialConfiguration) = 0; + /// Releases the height field object created by the physics backend. + /// @param nativeHeightfieldObject Pointer to the height field object. + virtual void ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject) = 0; + /// Releases the mesh object created by the physics backend. /// @param nativeMeshObject Pointer to the mesh object. virtual void ReleaseNativeMeshObject(void* nativeMeshObject) = 0; diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp b/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp index 3dcb37c541..d989847ae8 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp @@ -107,6 +107,7 @@ namespace Physics PhysicsAssetShapeConfiguration::Reflect(context); NativeShapeConfiguration::Reflect(context); CookedMeshShapeConfiguration::Reflect(context); + HeightfieldShapeConfiguration::Reflect(context); AzPhysics::SystemInterface::Reflect(context); AzPhysics::Scene::Reflect(context); AzPhysics::CollisionLayer::Reflect(context); diff --git a/Gems/AudioEngineWwise/Code/audioenginewwise_stub_files.cmake b/Code/Framework/AzFramework/AzFramework/Physics/physics_mock_files.cmake similarity index 83% rename from Gems/AudioEngineWwise/Code/audioenginewwise_stub_files.cmake rename to Code/Framework/AzFramework/AzFramework/Physics/physics_mock_files.cmake index 5597c28d04..162cc1ea86 100644 --- a/Gems/AudioEngineWwise/Code/audioenginewwise_stub_files.cmake +++ b/Code/Framework/AzFramework/AzFramework/Physics/physics_mock_files.cmake @@ -7,5 +7,5 @@ # set(FILES - Source/AudioEngineWwiseModule_Stub.cpp -) + Mocks/MockHeightfieldProviderBus.h +) \ No newline at end of file diff --git a/Code/Framework/AzFramework/AzFramework/Process/ProcessWatcher.cpp b/Code/Framework/AzFramework/AzFramework/Process/ProcessWatcher.cpp index 3ba7ed7223..798b1fe99f 100644 --- a/Code/Framework/AzFramework/AzFramework/Process/ProcessWatcher.cpp +++ b/Code/Framework/AzFramework/AzFramework/Process/ProcessWatcher.cpp @@ -22,7 +22,7 @@ namespace AzFramework AZStd::scoped_ptr pWatcher(LaunchProcess(processLaunchInfo, communicationType)); if (!pWatcher) { - AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process '%s %s'", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str()); + AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process '%s %s'\n", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str()); return false; } else @@ -31,7 +31,7 @@ namespace AzFramework ProcessCommunicator* pCommunicator = pWatcher->GetCommunicator(); if (!pCommunicator || !pCommunicator->IsValid()) { - AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: No communicator for watcher's process (%s %s)!", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str()); + AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: No communicator for watcher's process (%s %s)!\n", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str()); return false; } else diff --git a/Code/Framework/AzFramework/AzFramework/SurfaceData/SurfaceData.cpp b/Code/Framework/AzFramework/AzFramework/SurfaceData/SurfaceData.cpp new file mode 100644 index 0000000000..55fec57bb6 --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/SurfaceData/SurfaceData.cpp @@ -0,0 +1,59 @@ +/* + * 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 AzFramework::SurfaceData +{ + void SurfaceTagWeight::Reflect(AZ::ReflectContext* context) + { + if (AZ::SerializeContext* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Field("m_surfaceType", &SurfaceTagWeight::m_surfaceType) + ->Field("m_weight", &SurfaceTagWeight::m_weight) + ; + } + + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class() + ->Attribute(AZ::Script::Attributes::Category, "SurfaceData") + ->Constructor() + ->Property("surfaceType", BehaviorValueProperty(&SurfaceTagWeight::m_surfaceType)) + ->Property("weight", BehaviorValueProperty(&SurfaceTagWeight::m_weight)) + ; + } + } + + void SurfacePoint::Reflect(AZ::ReflectContext* context) + { + if (AZ::SerializeContext* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Field("m_position", &SurfacePoint::m_position) + ->Field("m_normal", &SurfacePoint::m_normal) + ->Field("m_surfaceTags", &SurfacePoint::m_surfaceTags) + ; + } + + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class("AzFramework::SurfaceData::SurfacePoint") + ->Attribute(AZ::Script::Attributes::Category, "SurfaceData") + ->Constructor() + ->Property("position", BehaviorValueProperty(&SurfacePoint::m_position)) + ->Property("normal", BehaviorValueProperty(&SurfacePoint::m_normal)) + ->Property("surfaceTags", BehaviorValueProperty(&SurfacePoint::m_surfaceTags)) + ; + } + } + +} // namespace AzFramework::SurfaceData diff --git a/Code/Framework/AzFramework/AzFramework/SurfaceData/SurfaceData.h b/Code/Framework/AzFramework/AzFramework/SurfaceData/SurfaceData.h new file mode 100644 index 0000000000..77d9ae4239 --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/SurfaceData/SurfaceData.h @@ -0,0 +1,72 @@ +/* + * 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 AzFramework::SurfaceData +{ + namespace Constants + { + static constexpr const char* s_unassignedTagName = "(unassigned)"; + } + + struct SurfaceTagWeight + { + AZ_TYPE_INFO(SurfaceTagWeight, "{EA14018E-E853-4BF5-8E13-D83BB99A54CC}"); + SurfaceTagWeight() = default; + SurfaceTagWeight(AZ::Crc32 surfaceType, float weight) + : m_surfaceType(surfaceType) + , m_weight(weight) + { + } + + AZ::Crc32 m_surfaceType = AZ::Crc32(Constants::s_unassignedTagName); + float m_weight = 0.0f; //! A Value in the range [0.0f .. 1.0f] + + static void Reflect(AZ::ReflectContext* context); + }; + + struct SurfaceTagWeightComparator + { + bool operator()(const SurfaceTagWeight& tagWeight1, const SurfaceTagWeight& tagWeight2) const + { + // Return a deterministic sort order for surface tags from highest to lowest weight, with the surface types sorted + // in a predictable order when the weights are equal. The surface type sort order is meaningless since it is sorting CRC + // values, it's really just important for it to be stable. + // For the floating-point weight comparisons we use exact instead of IsClose value comparisons for a similar reason - we + // care about being sorted highest to lowest, but there's no inherent meaning in sorting surface types with *similar* weights + // together. + + if (tagWeight1.m_weight != tagWeight2.m_weight) + { + return tagWeight1.m_weight > tagWeight2.m_weight; + } + else + { + return tagWeight1.m_surfaceType > tagWeight2.m_surfaceType; + } + } + }; + + using SurfaceTagWeightList = AZStd::vector; + + struct SurfacePoint final + { + AZ_TYPE_INFO(SurfacePoint, "{331A3D0E-BB1D-47BF-96A2-249FAA0D720D}"); + + AZ::Vector3 m_position; + AZ::Vector3 m_normal; + SurfaceTagWeightList m_surfaceTags; + + static void Reflect(AZ::ReflectContext* context); + }; +} // namespace AzFramework::SurfaceData diff --git a/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.cpp b/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.cpp index 1fb29cfa30..561408db20 100644 --- a/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.cpp +++ b/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.cpp @@ -8,56 +8,33 @@ #include "TerrainDataRequestBus.h" #include +#include -namespace AzFramework +namespace AzFramework::Terrain { - namespace SurfaceData + void TerrainDataRequests::Reflect(AZ::ReflectContext* context) { - void SurfaceTagWeight::Reflect(AZ::ReflectContext* context) + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) { - if (AZ::SerializeContext* serializeContext = azrtti_cast(context)) - { - serializeContext->Class() - ->Field("m_surfaceType", &SurfaceTagWeight::m_surfaceType) - ->Field("m_weight", &SurfaceTagWeight::m_weight) - ; - } - - if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) - { - behaviorContext->Class("SurfaceTagWeight") - ->Property("m_surfaceType", BehaviorValueProperty(&SurfaceTagWeight::m_surfaceType)) - ->Property("m_weight", BehaviorValueProperty(&SurfaceTagWeight::m_weight)) - ; - } - } - } //namespace SurfaceData - - namespace Terrain - { - void TerrainDataRequests::Reflect(AZ::ReflectContext* context) - { - AzFramework::SurfaceData::SurfaceTagWeight::Reflect(context); - - if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) - { - behaviorContext->EBus("TerrainDataRequestBus") - ->Attribute(AZ::Script::Attributes::Category, "Terrain") - ->Event("GetHeight", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetHeight) - ->Event("GetHeightFromFloats", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetHeightFromFloats) - ->Event("GetMaxSurfaceWeight", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetMaxSurfaceWeight) - ->Event("GetMaxSurfaceWeightFromFloats", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetMaxSurfaceWeightFromFloats) - ->Event("GetIsHoleFromFloats", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetIsHoleFromFloats) - ->Event("GetNormal", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetNormal) - ->Event("GetNormalFromFloats", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetNormalFromFloats) - ->Event("GetTerrainAabb", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainAabb) - ->Event("GetTerrainHeightQueryResolution", - &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainHeightQueryResolution) - ; - - } - + behaviorContext->EBus("TerrainDataRequestBus") + ->Attribute(AZ::Script::Attributes::Category, "Terrain") + ->Event("GetHeight", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetHeight) + ->Event("GetNormal", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetNormal) + ->Event("GetMaxSurfaceWeight", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetMaxSurfaceWeight) + ->Event("GetMaxSurfaceWeightFromVector2", + &AzFramework::Terrain::TerrainDataRequestBus::Events::GetMaxSurfaceWeightFromVector2) + ->Event("GetSurfaceWeights", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetSurfaceWeights) + ->Event("GetSurfaceWeightsFromVector2", + &AzFramework::Terrain::TerrainDataRequestBus::Events::GetSurfaceWeightsFromVector2) + ->Event("GetIsHoleFromFloats", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetIsHoleFromFloats) + ->Event("GetSurfacePoint", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetSurfacePoint) + ->Event("GetSurfacePointFromVector2", + &AzFramework::Terrain::TerrainDataRequestBus::Events::GetSurfacePointFromVector2) + ->Event("GetTerrainAabb", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainAabb) + ->Event("GetTerrainHeightQueryResolution", + &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainHeightQueryResolution) + ; } - } //namespace Terrain -} // namespace AzFramework + } +} // namespace AzFramework::Terrain diff --git a/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h b/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h index 3f6a8f0960..30a2f8e044 100644 --- a/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h +++ b/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h @@ -8,50 +8,13 @@ #pragma once #include -#include #include #include #include -#include +#include namespace AzFramework { - namespace SurfaceData - { - namespace Constants - { - static const char* s_unassignedTagName = "(unassigned)"; - } - - struct SurfaceTagWeight - { - AZ_TYPE_INFO(SurfaceTagWeight, "{EA14018E-E853-4BF5-8E13-D83BB99A54CC}"); - - AZ::Crc32 m_surfaceType = AZ::Crc32(Constants::s_unassignedTagName); - float m_weight = 0.0f; //! A Value in the range [0.0f .. 1.0f] - - //! Don't call this directly. TerrainDataRequests::Reflect is doing it already. - static void Reflect(AZ::ReflectContext* context); - }; - - struct SurfaceTagWeightComparator - { - bool operator()(const SurfaceTagWeight& tagWeight1, const SurfaceTagWeight& tagWeight2) const - { - if (!AZ::IsClose(tagWeight1.m_weight, tagWeight2.m_weight)) - { - return tagWeight1.m_weight > tagWeight2.m_weight; - } - else - { - return tagWeight1.m_surfaceType > tagWeight2.m_surfaceType; - } - } - }; - - using OrderedSurfaceTagWeightSet = AZStd::set; - } //namespace SurfaceData - namespace Terrain { @@ -91,49 +54,82 @@ namespace AzFramework //! Returns terrains height in meters at location x,y. //! @terrainExistsPtr: Can be nullptr. If != nullptr then, if there's no terrain at location x,y or location x,y is inside a terrain HOLE then *terrainExistsPtr will become false, //! otherwise *terrainExistsPtr will become true. - virtual float GetHeight(AZ::Vector3 position, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; - virtual float GetHeightFromFloats(float x, float y, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; + virtual float GetHeight(const AZ::Vector3& position, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; + virtual float GetHeightFromVector2( + const AZ::Vector2& position, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; + virtual float GetHeightFromFloats( + float x, float y, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; + + //! Returns true if there's a hole at location x,y. + //! Also returns true if there's no terrain data at location x,y. + virtual bool GetIsHole(const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR) const = 0; + virtual bool GetIsHoleFromVector2(const AZ::Vector2& position, Sampler sampleFilter = Sampler::BILINEAR) const = 0; + virtual bool GetIsHoleFromFloats(float x, float y, Sampler sampleFilter = Sampler::BILINEAR) const = 0; + + // Given an XY coordinate, return the surface normal. + //! @terrainExists: Can be nullptr. If != nullptr then, if there's no terrain at location x,y or location x,y is inside a + //! terrain HOLE then *terrainExistsPtr will be set to false, + //! otherwise *terrainExistsPtr will be set to true. + virtual AZ::Vector3 GetNormal( + const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; + virtual AZ::Vector3 GetNormalFromVector2( + const AZ::Vector2& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; + virtual AZ::Vector3 GetNormalFromFloats( + float x, float y, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; //! Given an XY coordinate, return the max surface type and weight. //! @terrainExists: Can be nullptr. If != nullptr then, if there's no terrain at location x,y or location x,y is inside a terrain HOLE then *terrainExistsPtr will be set to false, //! otherwise *terrainExistsPtr will be set to true. - virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeight(AZ::Vector3 position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; - virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeightFromVector2(const AZ::Vector2& inPosition, Sampler sampleFilter = Sampler::DEFAULT, bool* terrainExistsPtr = nullptr) const = 0; - virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeightFromFloats(float x, float y, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; + virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeight( + const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; + virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeightFromVector2( + const AZ::Vector2& inPosition, Sampler sampleFilter = Sampler::DEFAULT, bool* terrainExistsPtr = nullptr) const = 0; + virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeightFromFloats( + float x, float y, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; //! Given an XY coordinate, return the set of surface types and weights. The Vector3 input position version is defined to ignore //! the input Z value. virtual void GetSurfaceWeights( const AZ::Vector3& inPosition, - SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights, + SurfaceData::SurfaceTagWeightList& outSurfaceWeights, Sampler sampleFilter = Sampler::DEFAULT, bool* terrainExistsPtr = nullptr) const = 0; virtual void GetSurfaceWeightsFromVector2( const AZ::Vector2& inPosition, - SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights, + SurfaceData::SurfaceTagWeightList& outSurfaceWeights, Sampler sampleFilter = Sampler::DEFAULT, bool* terrainExistsPtr = nullptr) const = 0; virtual void GetSurfaceWeightsFromFloats( float x, float y, - SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights, + SurfaceData::SurfaceTagWeightList& outSurfaceWeights, Sampler sampleFilter = Sampler::DEFAULT, bool* terrainExistsPtr = nullptr) const = 0; //! Convenience function for low level systems that can't do a reverse lookup from Crc to string. Everyone else should use GetMaxSurfaceWeight or GetMaxSurfaceWeightFromFloats. //! Not available in the behavior context. //! Returns nullptr if the position is inside a hole or outside of the terrain boundaries. - virtual const char * GetMaxSurfaceName(AZ::Vector3 position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; - - //! Returns true if there's a hole at location x,y. - //! Also returns true if there's no terrain data at location x,y. - virtual bool GetIsHoleFromFloats(float x, float y, Sampler sampleFilter = Sampler::BILINEAR) const = 0; + virtual const char* GetMaxSurfaceName( + const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; - // Given an XY coordinate, return the surface normal. - //! @terrainExists: Can be nullptr. If != nullptr then, if there's no terrain at location x,y or location x,y is inside a terrain HOLE then *terrainExistsPtr will be set to false, - //! otherwise *terrainExistsPtr will be set to true. - virtual AZ::Vector3 GetNormal(AZ::Vector3 position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; - virtual AZ::Vector3 GetNormalFromFloats(float x, float y, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0; + //! Given an XY coordinate, return all terrain information at that location. The Vector3 input position version is defined + //! to ignore the input Z value. + virtual void GetSurfacePoint( + const AZ::Vector3& inPosition, + SurfaceData::SurfacePoint& outSurfacePoint, + Sampler sampleFilter = Sampler::DEFAULT, + bool* terrainExistsPtr = nullptr) const = 0; + virtual void GetSurfacePointFromVector2( + const AZ::Vector2& inPosition, + SurfaceData::SurfacePoint& outSurfacePoint, + Sampler sampleFilter = Sampler::DEFAULT, + bool* terrainExistsPtr = nullptr) const = 0; + virtual void GetSurfacePointFromFloats( + float x, + float y, + SurfaceData::SurfacePoint& outSurfacePoint, + Sampler sampleFilter = Sampler::DEFAULT, + bool* terrainExistsPtr = nullptr) const = 0; }; using TerrainDataRequestBus = AZ::EBus; @@ -169,6 +165,10 @@ namespace AzFramework } }; using TerrainDataNotificationBus = AZ::EBus; - - } //namespace Terrain + } // namespace Terrain } // namespace AzFramework + +namespace AZ +{ + AZ_TYPE_INFO_SPECIALIZE(AzFramework::Terrain::TerrainDataRequests::Sampler, "{D29BB6D7-3006-4114-858D-355EAA256B86}"); +} // namespace AZ diff --git a/Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.cpp b/Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.cpp index b4cad8511f..cd52393df9 100644 --- a/Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.cpp +++ b/Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.cpp @@ -14,9 +14,8 @@ namespace AzFramework { AZ_CVAR(bool, bg_octreeUseQuadtree, false, nullptr, AZ::ConsoleFunctorFlags::ReadOnly, "If set to true, the visibility octrees will degenerate to a quadtree split along the X/Y plane"); AZ_CVAR(float, bg_octreeMaxWorldExtents, 16384.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum supported world size by the world octreeSystemComponent"); - AZ_CVAR(uint32_t, bg_octreeNodeMaxEntries, 64, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum number of entries to allow in any node before forcing a split"); - AZ_CVAR(uint32_t, bg_octreeNodeMinEntries, 32, nullptr, AZ::ConsoleFunctorFlags::Null, "Minimum number of entries to allow in a node resulting from a merge operation"); - + AZ_CVAR(uint32_t, bg_octreeNodeMaxEntries, 64, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum number of entries to allow in any node before forcing a split"); + AZ_CVAR(uint32_t, bg_octreeNodeMinEntries, 32, nullptr, AZ::ConsoleFunctorFlags::Null, "Minimum number of entries to allow in a node resulting from a merge operation"); static uint32_t GetChildNodeCount() { @@ -25,14 +24,12 @@ namespace AzFramework return (bg_octreeUseQuadtree) ? QuadtreeNodeChildCount : OctreeNodeChildCount; } - OctreeNode::OctreeNode(const AZ::Aabb& bounds) : m_bounds(bounds) { ; } - OctreeNode::OctreeNode(OctreeNode&& rhs) : m_bounds(rhs.m_bounds) , m_parent(rhs.m_parent) @@ -46,7 +43,6 @@ namespace AzFramework } } - OctreeNode& OctreeNode::operator=(OctreeNode&& rhs) { m_bounds = rhs.m_bounds; @@ -63,7 +59,6 @@ namespace AzFramework return *this; } - void OctreeNode::Insert(OctreeScene& octreeScene, VisibilityEntry* entry) { AZ_Assert(entry->m_internalNode == nullptr, "Double-insertion: Insert invoked for an entry already bound to the OctreeScene"); @@ -98,7 +93,6 @@ namespace AzFramework } } - void OctreeNode::Update(OctreeScene& octreeScene, VisibilityEntry* entry) { AZ_Assert(entry->m_internalNode == this, "Update invoked for an entry bound to a different OctreeNode"); @@ -129,7 +123,6 @@ namespace AzFramework } } - void OctreeNode::Remove(OctreeScene& octreeScene, VisibilityEntry* entry) { AZ_Assert(entry->m_internalNode == this, "Remove invoked for an entry bound to a different OctreeNode"); @@ -152,25 +145,30 @@ namespace AzFramework } } - void OctreeNode::Enumerate(const AZ::Aabb& aabb, const IVisibilityScene::EnumerateCallback& callback) const { - EnumerateHelper(aabb, callback); + if (AZ::ShapeIntersection::Overlaps(aabb, m_bounds)) + { + EnumerateHelper(aabb, callback); + } } - void OctreeNode::Enumerate(const AZ::Sphere& sphere, const IVisibilityScene::EnumerateCallback& callback) const { - EnumerateHelper(sphere, callback); + if (AZ::ShapeIntersection::Overlaps(sphere, m_bounds)) + { + EnumerateHelper(sphere, callback); + } } - void OctreeNode::Enumerate(const AZ::Frustum& frustum, const IVisibilityScene::EnumerateCallback& callback) const { - EnumerateHelper(frustum, callback); + if (AZ::ShapeIntersection::Overlaps(frustum, m_bounds)) + { + EnumerateHelper(frustum, callback); + } } - void OctreeNode::EnumerateNoCull(const IVisibilityScene::EnumerateCallback& callback) const { // Invoke the callback for the current node @@ -190,25 +188,21 @@ namespace AzFramework } } - const AZStd::vector& OctreeNode::GetEntries() const { return m_entries; } - OctreeNode* OctreeNode::GetChildren() const { return m_children; } - bool OctreeNode::IsLeaf() const { return m_children == nullptr; } - void OctreeNode::TryMerge(OctreeScene& octreeScene) { if (IsLeaf()) @@ -236,7 +230,6 @@ namespace AzFramework } } - template void OctreeNode::EnumerateHelper(const T& boundingVolume, const IVisibilityScene::EnumerateCallback& callback) const { @@ -262,7 +255,6 @@ namespace AzFramework } } - void OctreeNode::Split(OctreeScene& octreeScene) { AZ_Assert(m_children == nullptr, "Split invoked on an octreeScene node that has already been split"); @@ -312,7 +304,6 @@ namespace AzFramework } } - void OctreeNode::Merge(OctreeScene& octreeScene) { AZ_Assert(m_children != nullptr, "Merge invoked on an octreeScene node that does not have children"); @@ -371,7 +362,6 @@ namespace AzFramework } } - void OctreeScene::RemoveEntry(VisibilityEntry& entry) { AZStd::lock_guard lock(m_sharedMutex); @@ -382,35 +372,30 @@ namespace AzFramework } } - void OctreeScene::Enumerate(const AZ::Aabb& aabb, const IVisibilityScene::EnumerateCallback& callback) const { AZStd::shared_lock lock(m_sharedMutex); m_root.Enumerate(aabb, callback); } - void OctreeScene::Enumerate(const AZ::Sphere& sphere, const IVisibilityScene::EnumerateCallback& callback) const { AZStd::shared_lock lock(m_sharedMutex); m_root.Enumerate(sphere, callback); } - void OctreeScene::Enumerate(const AZ::Frustum& frustum, const IVisibilityScene::EnumerateCallback& callback) const { AZStd::shared_lock lock(m_sharedMutex); m_root.Enumerate(frustum, callback); } - void OctreeScene::EnumerateNoCull(const IVisibilityScene::EnumerateCallback& callback) const { AZStd::shared_lock lock(m_sharedMutex); m_root.EnumerateNoCull(callback); } - uint32_t OctreeScene::GetEntryCount() const { return m_entryCount; @@ -421,26 +406,22 @@ namespace AzFramework return m_nodeCount; } - uint32_t OctreeScene::GetFreeNodeCount() const { // Each entry represents GetChildNodeCount() nodes return aznumeric_cast(m_freeOctreeNodes.size() * GetChildNodeCount()); } - uint32_t OctreeScene::GetPageCount() const { return aznumeric_cast(m_nodeCache.size()); } - uint32_t OctreeScene::GetChildNodeCount() const { return AzFramework::GetChildNodeCount(); } - void OctreeScene::DumpStats() { AZ_TracePrintf("Console", "OctreeScene[\"%s\"]::EntryCount = %u", GetName().GetCStr(), GetEntryCount()); @@ -450,21 +431,18 @@ namespace AzFramework AZ_TracePrintf("Console", "OctreeScene[\"%s\"]::ChildNodeCount = %u", GetName().GetCStr(), GetChildNodeCount()); } - static inline uint32_t CreateNodeIndex(uint32_t page, uint32_t offset) { AZ_Assert(page <= 0xFFFF && offset <= 0xFFFF, "Out of range values passed to CreateNodeIndex"); return (page << 16) | offset; } - static inline void ExtractPageAndOffsetFromIndex(uint32_t index, uint32_t& page, uint32_t& offset) { offset = index & 0x0000FFFF; page = index >> 16; } - uint32_t OctreeScene::AllocateChildNodes() { const uint32_t childCount = GetChildNodeCount(); @@ -508,14 +486,12 @@ namespace AzFramework return CreateNodeIndex(nextChildPage, nextChildOffset); } - void OctreeScene::ReleaseChildNodes(uint32_t nodeIndex) { m_nodeCount -= GetChildNodeCount(); m_freeOctreeNodes.push(nodeIndex); } - OctreeNode* OctreeScene::GetChildNodesAtIndex(uint32_t nodeIndex) const { uint32_t childPage; @@ -524,7 +500,6 @@ namespace AzFramework return &(*m_nodeCache[childPage])[childOffset]; } - void OctreeSystemComponent::Reflect(AZ::ReflectContext* context) { if (auto* serializeContext = azrtti_cast(context)) @@ -534,19 +509,16 @@ namespace AzFramework } } - void OctreeSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { provided.push_back(AZ_CRC("OctreeService")); } - void OctreeSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) { incompatible.push_back(AZ_CRC("OctreeService")); } - OctreeSystemComponent::OctreeSystemComponent() { AZ::Interface::Register(this); @@ -555,7 +527,6 @@ namespace AzFramework m_defaultScene = aznew OctreeScene(AZ::Name("DefaultVisibilityScene")); } - OctreeSystemComponent::~OctreeSystemComponent() { AZ_Assert(m_scenes.empty(), "All IVisibilityScenes must be destroyed before shutdown"); @@ -566,13 +537,11 @@ namespace AzFramework AZ::Interface::Unregister(this); } - void OctreeSystemComponent::Activate() { ; } - void OctreeSystemComponent::Deactivate() { ; @@ -591,7 +560,6 @@ namespace AzFramework return newScene; } - void OctreeSystemComponent::DestroyVisibilityScene(IVisibilityScene* visScene) { for (auto iter = m_scenes.begin(); iter != m_scenes.end(); ++iter) @@ -606,7 +574,6 @@ namespace AzFramework AZ_Assert(false, "visScene[\"%s\"] not found in the OctreeSystemComponent", visScene->GetName().GetCStr()); } - IVisibilityScene* OctreeSystemComponent::FindVisibilityScene(const AZ::Name& sceneName) { for (OctreeScene* scene : m_scenes) @@ -619,7 +586,6 @@ namespace AzFramework return nullptr; } - void OctreeSystemComponent::DumpStats([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments) { for (OctreeScene* scene : m_scenes) diff --git a/Code/Framework/AzFramework/AzFramework/azframework_files.cmake b/Code/Framework/AzFramework/AzFramework/azframework_files.cmake index c5616d84c0..e03d166cfc 100644 --- a/Code/Framework/AzFramework/AzFramework/azframework_files.cmake +++ b/Code/Framework/AzFramework/AzFramework/azframework_files.cmake @@ -228,6 +228,7 @@ set(FILES Physics/Configuration/SimulatedBodyConfiguration.cpp Physics/Configuration/SystemConfiguration.h Physics/Configuration/SystemConfiguration.cpp + Physics/HeightfieldProviderBus.h Physics/SimulatedBodies/RigidBody.h Physics/SimulatedBodies/RigidBody.cpp Physics/SimulatedBodies/StaticRigidBody.h @@ -299,6 +300,8 @@ set(FILES Spawnable/SpawnableMonitor.cpp Spawnable/SpawnableSystemComponent.h Spawnable/SpawnableSystemComponent.cpp + SurfaceData/SurfaceData.h + SurfaceData/SurfaceData.cpp Terrain/TerrainDataRequestBus.h Terrain/TerrainDataRequestBus.cpp Thermal/ThermalInfo.h diff --git a/Code/Framework/AzFramework/CMakeLists.txt b/Code/Framework/AzFramework/CMakeLists.txt index b22586162b..c8eeac5c2d 100644 --- a/Code/Framework/AzFramework/CMakeLists.txt +++ b/Code/Framework/AzFramework/CMakeLists.txt @@ -42,6 +42,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) NAMESPACE AZ FILES_CMAKE Tests/framework_shared_tests_files.cmake + AzFramework/Physics/physics_mock_files.cmake INCLUDE_DIRECTORIES PUBLIC Tests @@ -53,7 +54,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) AZ::AzTest AZ::AzTestShared ) - + if(PAL_TRAIT_BUILD_HOST_TOOLS) ly_add_target( diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp index af5b3af3d8..79a6333612 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp @@ -308,7 +308,7 @@ namespace AzFramework event.data.data32[2] = 0; event.data.data32[3] = 1; event.data.data32[4] = 0; - xcb_void_cookie_t xcbCheckResult = xcb_send_event( + [[maybe_unused]] xcb_void_cookie_t xcbCheckResult = xcb_send_event( m_xcbConnection, 1, m_xcbRootScreen->root, XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, (const char*)&event); AZ_Assert(ValidateXcbResult(xcbCheckResult), "Failed to set _NET_WM_STATE_FULLSCREEN"); @@ -333,7 +333,7 @@ namespace AzFramework event.data.data32[2] = 0; event.data.data32[3] = 0; event.data.data32[4] = 0; - xcb_void_cookie_t xcbCheckResult = xcb_send_event( + [[maybe_unused]] xcb_void_cookie_t xcbCheckResult = xcb_send_event( m_xcbConnection, 1, m_xcbRootScreen->root, XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, (const char*)&event); AZ_Assert( diff --git a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp index 158240210e..85669e093c 100644 --- a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp +++ b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp @@ -7,17 +7,35 @@ */ #include +#include #if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB #include #endif +constexpr rlim_t g_minimumOpenFileHandles = 65536L; + //////////////////////////////////////////////////////////////////////////////////////////////////// namespace AzFramework { //////////////////////////////////////////////////////////////////////////////////////////////// Application::Implementation* Application::Implementation::Create() { + // The default open file limit for processes may not be enough for O3DE applications. + // We will need to increase to the recommended value if the current open file limit + // is not sufficient. + rlimit currentLimit; + int get_limit_result = getrlimit(RLIMIT_NOFILE, ¤tLimit); + AZ_Warning("Application", get_limit_result == 0, "Unable to read current ulimit open file limits"); + if ((get_limit_result == 0) && (currentLimit.rlim_cur < g_minimumOpenFileHandles || currentLimit.rlim_max < g_minimumOpenFileHandles)) + { + rlimit newLimit; + newLimit.rlim_cur = g_minimumOpenFileHandles; // Soft Limit + newLimit.rlim_max = g_minimumOpenFileHandles; // Hard Limit + [[maybe_unused]] int set_limit_result = setrlimit(RLIMIT_NOFILE, &newLimit); + AZ_Assert(set_limit_result == 0, "Unable to update open file limits"); + } + #if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB return aznew XcbApplication(); #elif PAL_TRAIT_LINUX_WINDOW_MANAGER_WAYLAND diff --git a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Process/ProcessWatcher_Linux.cpp b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Process/ProcessWatcher_Linux.cpp index d2cd681012..51a8545443 100644 --- a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Process/ProcessWatcher_Linux.cpp +++ b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Process/ProcessWatcher_Linux.cpp @@ -91,31 +91,42 @@ namespace AzFramework return processId == 0; } - /*! Executes a command in the child process after the fork operation has been executed. - * This function will never return. If the execvp command fails this will call _exit with - * the errno value as the return value since continuing execution after a execvp command - * is invalid (it will be running the parent's code and in its address space and will - * cause many issues). + /*! Executes a command in the child process after the fork operation + * has been executed. This function will never return. If the execvpe + * command fails this will call _exit since continuing execution after + * a execvpe command is invalid (it will be running the parent's code + * and in its address space and will cause many issues). + * + * This function runs after a `fork()` call. `fork()` creates a copy of + * the current process, including the current state of the process's + * memory, at the time the call is made. However, it only creates a + * copy of the one thread that called `fork()`. This means that if any + * mutexes are locked by other threads at the time of `fork()`, those + * mutexes will remain locked in the child process, with no way to + * unlock them. So this function needs to ensure that it does as little + * work as possible. * * \param commandAndArgs - Array of strings that has the command to execute in index 0 with any args for the command following. Last element must be a null pointer. - * \param envionrmentVariables - Array of strings that contains environment variables that command should use. Last element must be a null pointer. + * \param environmentVariables - Array of strings that contains environment variables that command should use. Last element must be a null pointer. * \param processLaunchInfo - struct containing information about luanching the command * \param startupInfo - struct containing information needed to startup the command + * \param errorPipe - a pipe file descriptor used to communicate a failed execvpe call's error code to the parent process */ - void ExecuteCommandAsChild(char** commandAndArgs, char** environmentVariables, const ProcessLauncher::ProcessLaunchInfo& processLaunchInfo, StartupInfo& startupInfo) + [[noreturn]] static void ExecuteCommandAsChild(char** commandAndArgs, char** environmentVariables, const ProcessLauncher::ProcessLaunchInfo& processLaunchInfo, StartupInfo& startupInfo, const AZStd::array& errorPipe) { + close(errorPipe[0]); + if (!processLaunchInfo.m_workingDirectory.empty()) { int res = chdir(processLaunchInfo.m_workingDirectory.c_str()); if (res != 0) { - std::cerr << strerror(errno) << std::endl; - AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to change the launched process' directory to '%s'.", processLaunchInfo.m_workingDirectory.c_str()); + write(errorPipe[1], &errno, sizeof(int)); // We *have* to _exit as we are the child process and simply // returning at this point would mean we would start running // the code from our parent process and that will just wreck // havoc. - _exit(errno); + _exit(0); } } @@ -135,15 +146,17 @@ namespace AzFramework startupInfo.SetupHandlesForChildProcess(); - execve(commandAndArgs[0], commandAndArgs, environmentVariables); + execvpe(commandAndArgs[0], commandAndArgs, environmentVariables); + const int errval = errno; - // If we get here then execve failed to run the requested program and + // If we get here then execvpe failed to run the requested program and // we have an error. In this case we need to exit the child process - // to stop it from continuing to run as a clone of the parent - AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process %s : errno = %s ", commandAndArgs[0], strerror(errno)); - std::cerr << strerror(errno) << std::endl; + // to stop it from continuing to run as a clone of the parent. + // Communicate the error code back to the parent via a pipe for the + // parent to read. + write(errorPipe[1], &errval, sizeof(errval)); - _exit(errno); + _exit(0); } } @@ -212,9 +225,8 @@ namespace AzFramework AZStd::string outputString; bool inQuotes = false; - for (size_t pos = 0; pos < processLaunchInfo.m_commandlineParameters.size(); ++pos) + for (const char currentChar : processLaunchInfo.m_commandlineParameters) { - char currentChar = processLaunchInfo.m_commandlineParameters[pos]; if (currentChar == '"') { inQuotes = !inQuotes; @@ -231,7 +243,7 @@ namespace AzFramework outputString.push_back(currentChar); } } - + if (!outputString.empty()) { commandTokens.push_back(outputString); @@ -249,10 +261,10 @@ namespace AzFramework return false; } - // Because of the way execve is defined we need to copy the strings from + // Because of the way execvpe is defined we need to copy the strings from // AZ::string (using c_str() returns a const char*) into a non-const char* - // Need to add one more as exec requires the array's last element to be a null pointer + // Need to add one more as execvpe requires the array's last element to be a null pointer char** commandAndArgs = new char*[commandTokens.size() + 1]; for (int i = 0; i < commandTokens.size(); ++i) { @@ -275,7 +287,7 @@ namespace AzFramework azstrcat(environmentVariable.get(), envVarString.size() + 1, envVarString.c_str()); environmentVariablesVector.emplace_back(environmentVariable.get()); } - // Adding one more as exec expects the array to have a nullptr as the last element + // Adding one more as execvpe expects the array to have a nullptr as the last element environmentVariablesVector.emplace_back(nullptr); environmentVariables = environmentVariablesVector.data(); } @@ -288,15 +300,50 @@ namespace AzFramework AZ_Assert(environmentVariables, "Environment variables for current process not available\n"); } + // Set up a pipe to communicate the error code from the subprocess's execvpe call + AZStd::array childErrorPipeFds{}; + pipe(childErrorPipeFds.data()); + + // This configures the write end of the pipe to close on calls to `exec` + fcntl(childErrorPipeFds[1], F_SETFD, fcntl(childErrorPipeFds[1], F_GETFD) | FD_CLOEXEC); + pid_t child_pid = fork(); if (IsIdChildProcess(child_pid)) { - ExecuteCommandAsChild(commandAndArgs, environmentVariables, processLaunchInfo, processData.m_startupInfo); + ExecuteCommandAsChild(commandAndArgs, environmentVariables, processLaunchInfo, processData.m_startupInfo, childErrorPipeFds); } - processData.m_childProcessId = child_pid; // Close these handles as they are only to be used by the child process processData.m_startupInfo.CloseAllHandles(); + close(childErrorPipeFds[1]); + + { + int errorCodeFromChild = 0; + int count = 0; + // Read from the error pipe. + // * If the child's call to execvpe succeeded, then the pipe will + // be closed due to setting FD_CLOEXEC on the write end of the + // pipe. `read()` will return 0. + // * If the child's call to execvpe failed, the child will have + // written the error code to the pipe. `read()` will return >0, and + // the data to be read is the error code from execvpe. + while ((count = read(childErrorPipeFds[0], &errorCodeFromChild, sizeof(errorCodeFromChild))) == -1) + { + if (errno != EAGAIN && errno != EINTR) + { + break; + } + } + if (count) + { + AZ_TracePrintf("Process Watcher", "ProcessLauncher::LaunchProcess: Unable to launch process %s : errno = %s\n", commandAndArgs[0], strerror(errorCodeFromChild)); + processData.m_childProcessIsDone = true; + child_pid = -1; + } + } + close(childErrorPipeFds[0]); + + processData.m_childProcessId = child_pid; for (int i = 0; i < commandTokens.size(); i++) { diff --git a/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.cpp b/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.cpp index 88392c98c0..b0f316cf50 100644 --- a/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.cpp +++ b/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.cpp @@ -53,7 +53,7 @@ namespace AzNetworking m_timeoutItemMap.erase(timeoutId); } - void TimeoutQueue::UpdateTimeouts(ITimeoutHandler& timeoutHandler, int32_t maxTimeouts) + void TimeoutQueue::UpdateTimeouts(const TimeoutHandler& timeoutHandler, int32_t maxTimeouts) { int32_t numTimeouts = 0; if (maxTimeouts < 0) @@ -103,7 +103,7 @@ namespace AzNetworking // By this point, the item is definitely timed out // Invoke the timeout function to see how to proceed - const TimeoutResult result = timeoutHandler.HandleTimeout(mapItem); + const TimeoutResult result = timeoutHandler(mapItem); if (result == TimeoutResult::Refresh) { @@ -122,4 +122,10 @@ namespace AzNetworking m_timeoutItemMap.erase(itemTimeoutId); } } + + void TimeoutQueue::UpdateTimeouts(ITimeoutHandler& timeoutHandler, int32_t maxTimeouts) + { + TimeoutHandler handler([&timeoutHandler](TimeoutQueue::TimeoutItem& item) { return timeoutHandler.HandleTimeout(item); }); + UpdateTimeouts(handler, maxTimeouts); + } } diff --git a/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.h b/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.h index 239af5be0e..63417ea36f 100644 --- a/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.h +++ b/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.h @@ -64,6 +64,12 @@ namespace AzNetworking //! @param timeoutId the identifier of the item to remove void RemoveItem(TimeoutId timeoutId); + //! Updates timeouts for all items, invokes the provided timeout functor if required. + //! @param timeoutHandler lambda to invoke for all timeouts + //! @param maxTimeouts the maximum number of timeouts to process before breaking iteration + using TimeoutHandler = AZStd::function; + void UpdateTimeouts(const TimeoutHandler& timeoutHandler, int32_t maxTimeouts = -1); + //! Updates timeouts for all items, invokes timeout handlers if required. //! @param timeoutHandler listener instance to call back on for timeouts //! @param maxTimeouts the maximum number of timeouts to process before breaking iteration diff --git a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/ToastNotification/ToastNotification.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp similarity index 71% rename from Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/ToastNotification/ToastNotification.cpp rename to Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp index 41c2d8cecd..f79f355ccb 100644 --- a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/ToastNotification/ToastNotification.cpp +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp @@ -5,24 +5,20 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT * */ -#include - #include +#include +#include -#include -#include - -#include +#include +#include +#include +#include +#include -namespace GraphCanvas +namespace AzQtComponents { - ////////////////////// - // ToastNotification - ////////////////////// - ToastNotification::ToastNotification(QWidget* parent, const ToastConfiguration& toastConfiguration) : QDialog(parent, Qt::FramelessWindowHint) - , m_toastId(AZ::Entity::MakeId()) , m_closeOnClick(true) , m_ui(new Ui::ToastNotification()) , m_fadeAnimation(nullptr) @@ -32,39 +28,53 @@ namespace GraphCanvas setAttribute(Qt::WA_ShowWithoutActivating); setAttribute(Qt::WA_DeleteOnClose); + m_borderRadius = toastConfiguration.m_borderRadius; + if (m_borderRadius > 0) + { + setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog); + setAttribute(Qt::WA_TranslucentBackground); + } + m_ui->setupUi(this); QIcon toastIcon; - switch (toastConfiguration.GetToastType()) + switch (toastConfiguration.m_toastType) { case ToastType::Error: - toastIcon = QIcon(":/GraphCanvasEditorResources/toast_error_icon.png"); + toastIcon = QIcon(":/stylesheet/img/logging/error.svg"); break; case ToastType::Warning: - toastIcon = QIcon(":/GraphCanvasEditorResources/toast_warning_icon.png"); + toastIcon = QIcon(":/stylesheet/img/logging/warning-yellow.svg"); break; case ToastType::Information: - toastIcon = QIcon(":/GraphCanvasEditorResources/toast_information_icon.png"); + toastIcon = QIcon(":/stylesheet/img/logging/information.svg"); break; case ToastType::Custom: - toastIcon = QIcon(toastConfiguration.GetCustomToastImage().c_str()); + toastIcon = QIcon(toastConfiguration.m_customIconImage); default: break; } m_ui->iconLabel->setPixmap(toastIcon.pixmap(64, 64)); - m_ui->titleLabel->setText(toastConfiguration.GetTitleLabel().c_str()); - m_ui->mainLabel->setText(toastConfiguration.GetDescriptionLabel().c_str()); + m_ui->titleLabel->setText(toastConfiguration.m_title); + m_ui->mainLabel->setText(toastConfiguration.m_description); - m_lifeSpan.setInterval(aznumeric_cast(toastConfiguration.GetDuration().count())); - m_closeOnClick = toastConfiguration.GetCloseOnClick(); + // hide the optional description if none is provided so the title is centered vertically + if (toastConfiguration.m_description.isEmpty()) + { + m_ui->mainLabel->setVisible(false); + m_ui->verticalLayout->removeWidget(m_ui->mainLabel); + } + + m_lifeSpan.setInterval(aznumeric_cast(toastConfiguration.m_duration.count())); + m_closeOnClick = toastConfiguration.m_closeOnClick; m_ui->closeButton->setVisible(m_closeOnClick); QObject::connect(m_ui->closeButton, &QToolButton::clicked, this, &ToastNotification::accept); - m_fadeDuration = toastConfiguration.GetFadeDuration(); + m_fadeDuration = toastConfiguration.m_fadeDuration; QObject::connect(&m_lifeSpan, &QTimer::timeout, this, &ToastNotification::FadeOut); } @@ -73,9 +83,22 @@ namespace GraphCanvas { } - ToastId ToastNotification::GetToastId() const + void ToastNotification::paintEvent(QPaintEvent* event) { - return m_toastId; + if (m_borderRadius > 0) + { + QPainter p(this); + p.setPen(Qt::transparent); + QColor painterColor; + painterColor.setRgbF(0, 0, 0, 255); + p.setBrush(painterColor); + p.setRenderHint(QPainter::Antialiasing); + p.drawRoundedRect(rect(), m_borderRadius, m_borderRadius); + } + else + { + QDialog::paintEvent(event); + } } void ToastNotification::ShowToastAtCursor() @@ -131,8 +154,6 @@ namespace GraphCanvas { StartTimer(); } - - emit ToastNotificationShown(); } void ToastNotification::hideEvent(QHideEvent* hideEvent) @@ -147,7 +168,6 @@ namespace GraphCanvas delete m_fadeAnimation; } - ToastNotificationBus::Event(GetToastId(), &ToastNotifications::OnToastDismissed); emit ToastNotificationHidden(); } @@ -155,7 +175,7 @@ namespace GraphCanvas { if (m_closeOnClick) { - ToastNotificationBus::Event(GetToastId(), &ToastNotifications::OnToastInteraction); + emit ToastNotificationInteraction(); accept(); } } @@ -205,5 +225,5 @@ namespace GraphCanvas accept(); } } -#include +#include "Components/moc_ToastNotification.cpp" } diff --git a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/ToastNotification/ToastNotification.h b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h similarity index 79% rename from Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/ToastNotification/ToastNotification.h rename to Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h index 40debea0fe..7f2701a803 100644 --- a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/ToastNotification/ToastNotification.h +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h @@ -8,17 +8,13 @@ #pragma once #if !defined(Q_MOC_RUN) +#include +#include +#include #include #include #include -#include #include -#include -#include -#include -#include - -#include #endif namespace Ui @@ -26,20 +22,20 @@ namespace Ui class ToastNotification; } -namespace GraphCanvas +QT_FORWARD_DECLARE_CLASS(QPropertyAnimation) + +namespace AzQtComponents { - class ToastNotification + class AZ_QT_COMPONENTS_API ToastNotification : public QDialog { Q_OBJECT public: AZ_CLASS_ALLOCATOR(ToastNotification, AZ::SystemAllocator, 0); - ToastNotification(QWidget* parent, const ToastConfiguration& configuration); + ToastNotification(QWidget* parent, const ToastConfiguration& toastConfiguration); virtual ~ToastNotification(); - ToastId GetToastId() const; - // Shows the toast notification relative to the current cursor. void ShowToastAtCursor(); @@ -53,30 +49,29 @@ namespace GraphCanvas // QDialog void showEvent(QShowEvent* showEvent) override; void hideEvent(QHideEvent* hideEvent) override; - void mousePressEvent(QMouseEvent* mouseEvent) override; - bool eventFilter(QObject* object, QEvent* event) override; - //// - public slots: + void paintEvent(QPaintEvent* event) override; + public slots: void StartTimer(); void FadeOut(); signals: - void ToastNotificationShown(); void ToastNotificationHidden(); + void ToastNotificationInteraction(); private: - QPropertyAnimation* m_fadeAnimation; - AZStd::chrono::milliseconds m_fadeDuration; - - ToastId m_toastId; bool m_closeOnClick; QTimer m_lifeSpan; + uint32_t m_borderRadius = 0; + + AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING + AZStd::chrono::milliseconds m_fadeDuration; AZStd::unique_ptr m_ui; + AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING }; -} +} // namespace AzQtComponents diff --git a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/ToastNotification/ToastNotification.ui b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui similarity index 92% rename from Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/ToastNotification/ToastNotification.ui rename to Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui index 2c82f042b8..7aefddbd98 100644 --- a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/ToastNotification/ToastNotification.ui +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui @@ -101,7 +101,7 @@ - :/GraphCanvasEditorResources/toast_information_icon.png + :/stylesheet/img/logging/information.svg true @@ -191,7 +191,7 @@ - 40 + 20 20 @@ -203,8 +203,8 @@ ... - - :/GraphCanvasEditorResources/lineedit_clear.png:/GraphCanvasEditorResources/lineedit_clear.png + + :/stylesheet/img/close_x.svg:/stylesheet/img/close_x.svg @@ -230,8 +230,7 @@ - - + diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.cpp new file mode 100644 index 0000000000..41e3ea836c --- /dev/null +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.cpp @@ -0,0 +1,18 @@ +/* + * 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 AzQtComponents +{ + ToastConfiguration::ToastConfiguration(ToastType toastType, const QString& title, const QString& description) + : m_toastType(toastType) + , m_title(title) + , m_description(description) + { + } +} // namespace AzQtComponents diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h new file mode 100644 index 0000000000..5ace9d1be2 --- /dev/null +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h @@ -0,0 +1,47 @@ +/* + * 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 +#include +#include +#endif + +namespace AzQtComponents +{ + enum class ToastType + { + Information, + Warning, + Error, + Custom + }; + + class AZ_QT_COMPONENTS_API ToastConfiguration + { + public: + AZ_CLASS_ALLOCATOR(ToastConfiguration, AZ::SystemAllocator, 0); + ToastConfiguration(ToastType toastType, const QString& title, const QString& description); + + bool m_closeOnClick = true; + + ToastType m_toastType = ToastType::Information; + + QString m_title; + QString m_description; + QString m_customIconImage; + uint32_t m_borderRadius = 0; + + AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING + AZStd::chrono::milliseconds m_duration = AZStd::chrono::milliseconds(5000); + AZStd::chrono::milliseconds m_fadeDuration = AZStd::chrono::milliseconds(250); + AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING + }; +} // namespace AzQtComponents diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_close.svg b/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_close.svg new file mode 100644 index 0000000000..3fccf0e716 --- /dev/null +++ b/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_open.svg b/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_open.svg new file mode 100644 index 0000000000..55704ec230 --- /dev/null +++ b/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc b/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc index 0c8fedc79d..15049aeb69 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc +++ b/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc @@ -6,6 +6,8 @@ Entity/layer.svg Entity/prefab.svg Entity/prefab_edit.svg + Entity/prefab_edit_open.svg + Entity/prefab_edit_close.svg Level/level.svg diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Utilities/PixmapScaleUtilities.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Utilities/PixmapScaleUtilities.cpp index fa32b708d7..95c0289f16 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Utilities/PixmapScaleUtilities.cpp +++ b/Code/Framework/AzQtComponents/AzQtComponents/Utilities/PixmapScaleUtilities.cpp @@ -28,4 +28,21 @@ namespace AzQtComponents return scaledPixmap; } + + QPixmap CropPixmapForScreenDpi( + QPixmap pixmap, QScreen* screen, QRect rect) + { + qreal screenDpiFactor = QHighDpiScaling::factor(screen); + pixmap.setDevicePixelRatio(screenDpiFactor); + + QRect cropRect( + aznumeric_cast(aznumeric_cast(rect.left()) * screenDpiFactor), + aznumeric_cast(aznumeric_cast(rect.top()) * screenDpiFactor), + aznumeric_cast(aznumeric_cast(rect.width()) * screenDpiFactor), + aznumeric_cast(aznumeric_cast(rect.height()) * screenDpiFactor) + ); + + QPixmap croppedPixmap = pixmap.copy(cropRect); + return croppedPixmap; + } } diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Utilities/PixmapScaleUtilities.h b/Code/Framework/AzQtComponents/AzQtComponents/Utilities/PixmapScaleUtilities.h index 4b083855d5..4462e3c9a7 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Utilities/PixmapScaleUtilities.h +++ b/Code/Framework/AzQtComponents/AzQtComponents/Utilities/PixmapScaleUtilities.h @@ -11,9 +11,11 @@ #include #include +#include #include namespace AzQtComponents { AZ_QT_COMPONENTS_API QPixmap ScalePixmapForScreenDpi(QPixmap pixmap, QScreen* screen, QSize size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformationMode); + AZ_QT_COMPONENTS_API QPixmap CropPixmapForScreenDpi(QPixmap pixmap, QScreen* screen, QRect rect); }; // namespace AzQtComponents diff --git a/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_files.cmake b/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_files.cmake index 4c343b852c..4ed87ee358 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_files.cmake +++ b/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_files.cmake @@ -49,6 +49,11 @@ set(FILES Components/Titlebar.h Components/TitleBarOverdrawHandler.cpp Components/TitleBarOverdrawHandler.h + Components/ToastNotification.cpp + Components/ToastNotification.h + Components/ToastNotificationConfiguration.h + Components/ToastNotificationConfiguration.cpp + Components/ToastNotification.ui Components/ToolButtonComboBox.cpp Components/ToolButtonComboBox.h Components/ToolButtonLineEdit.cpp diff --git a/Code/Framework/AzTest/AzTest/Platform/Mac/Platform_Mac.cpp b/Code/Framework/AzTest/AzTest/Platform/Mac/Platform_Mac.cpp index 864bc3f52a..7581986950 100644 --- a/Code/Framework/AzTest/AzTest/Platform/Mac/Platform_Mac.cpp +++ b/Code/Framework/AzTest/AzTest/Platform/Mac/Platform_Mac.cpp @@ -7,6 +7,7 @@ */ #include #include +#include #include #include @@ -20,11 +21,16 @@ public: explicit ModuleHandle(const std::string& lib) : m_libHandle(nullptr) { - std::string libext = lib; - if (!AZ::Test::EndsWith(libext, ".dylib")) + AZ::IO::FixedMaxPath libext = AZStd::string_view{ lib.c_str(), lib.size() }; + if (!libext.Stem().Native().starts_with(AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX)) { - libext += ".dylib"; + libext = AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX + libext.Native(); } + if (libext.Extension() != AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION) + { + libext.Native() += AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION; + } + m_libHandle = dlopen(libext.c_str(), RTLD_NOW); const char* error = dlerror(); if (error) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Component/EditorComponentAPIBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Component/EditorComponentAPIBus.h index f7119f3e14..b639686269 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Component/EditorComponentAPIBus.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Component/EditorComponentAPIBus.h @@ -36,6 +36,10 @@ namespace AzToolsFramework //! the entity type. virtual AZStd::vector FindComponentTypeIdsByEntityType(const AZStd::vector& componentTypeNames, EntityType entityType) = 0; + //! Return a list of type ids for components that match the required services filter, + //! and don't conflict with any of the incompatible services filter + virtual AZStd::vector FindComponentTypeIdsByService(const AZStd::vector& serviceFilter, const AZStd::vector& incompatibleServiceFilter) = 0; + //! Finds the component names from their type ids virtual AZStd::vector FindComponentTypeNames(const AZ::ComponentTypeList& componentTypeIds) = 0; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Component/EditorComponentAPIComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Component/EditorComponentAPIComponent.cpp index 30a7c603bd..2cca3804ea 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Component/EditorComponentAPIComponent.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Component/EditorComponentAPIComponent.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -75,6 +76,7 @@ namespace AzToolsFramework serializeContext->Class(); serializeContext->RegisterGenericType>(); + serializeContext->RegisterGenericType>(); } if (auto behaviorContext = azrtti_cast(context)) @@ -99,6 +101,7 @@ namespace AzToolsFramework ->Attribute(AZ::Script::Attributes::Module, "editor") ->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All) ->Event("FindComponentTypeIdsByEntityType", &EditorComponentAPIRequests::FindComponentTypeIdsByEntityType) + ->Event("FindComponentTypeIdsByService", &EditorComponentAPIRequests::FindComponentTypeIdsByService) ->Event("FindComponentTypeNames", &EditorComponentAPIRequests::FindComponentTypeNames) ->Event("BuildComponentTypeNameListByEntityType", &EditorComponentAPIRequests::BuildComponentTypeNameListByEntityType) ->Event("AddComponentsOfType", &EditorComponentAPIRequests::AddComponentsOfType) @@ -216,6 +219,33 @@ namespace AzToolsFramework return foundTypeIds; } + AZStd::vector EditorComponentAPIComponent::FindComponentTypeIdsByService(const AZStd::vector& serviceFilter, const AZStd::vector& incompatibleServiceFilter) + { + AZStd::vector foundTypeIds; + + m_serializeContext->EnumerateDerived( + [&foundTypeIds, serviceFilter, incompatibleServiceFilter](const AZ::SerializeContext::ClassData* componentClass, const AZ::Uuid& knownType) -> bool + { + AZ_UNUSED(knownType); + + if (componentClass->m_editData) + { + // If none of the required services are offered by this component, or the component + // can not be added by the user, skip to the next component + if (!OffersRequiredServices(componentClass, serviceFilter, incompatibleServiceFilter)) + { + return true; + } + + foundTypeIds.push_back(componentClass->m_typeId); + } + + return true; + }); + + return foundTypeIds; + } + AZStd::vector EditorComponentAPIComponent::FindComponentTypeNames(const AZ::ComponentTypeList& componentTypeIds) { AZStd::vector foundTypeNames; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Component/EditorComponentAPIComponent.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Component/EditorComponentAPIComponent.h index 2b74ea98c5..3e7eede77f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Component/EditorComponentAPIComponent.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Component/EditorComponentAPIComponent.h @@ -35,6 +35,7 @@ namespace AzToolsFramework // EditorComponentAPIBus ... AZStd::vector FindComponentTypeIdsByEntityType(const AZStd::vector& componentTypeNames, EditorComponentAPIRequests::EntityType entityType) override; + AZStd::vector FindComponentTypeIdsByService(const AZStd::vector& serviceFilter, const AZStd::vector& incompatibleServiceFilter) override; AZStd::vector FindComponentTypeNames(const AZ::ComponentTypeList& componentTypeIds) override; AZStd::vector BuildComponentTypeNameListByEntityType(EditorComponentAPIRequests::EntityType entityType) override; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.cpp index 8d40162f52..28d6e2bc08 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.cpp @@ -271,6 +271,68 @@ namespace AzToolsFramework return editorComponentBaseComponent; } + bool OffersRequiredServices( + const AZ::SerializeContext::ClassData* componentClass, + const AZStd::vector& serviceFilter, + const AZStd::vector& incompatibleServiceFilter + ) + { + AZ_Assert(componentClass, "Component class must not be null"); + + if (!componentClass) + { + return false; + } + + AZ::ComponentDescriptor* componentDescriptor = nullptr; + AZ::ComponentDescriptorBus::EventResult( + componentDescriptor, componentClass->m_typeId, &AZ::ComponentDescriptor::GetDescriptor); + if (!componentDescriptor) + { + return false; + } + + // If no services are provided, this function returns true + if (serviceFilter.empty()) + { + return true; + } + + AZ::ComponentDescriptor::DependencyArrayType providedServices; + componentDescriptor->GetProvidedServices(providedServices, nullptr); + + //reject this component if it does not offer any of the required services + if (AZStd::find_first_of( + providedServices.begin(), + providedServices.end(), + serviceFilter.begin(), + serviceFilter.end()) == providedServices.end()) + { + return false; + } + + //reject this component if it does offer any of the incompatible services + if (AZStd::find_first_of( + providedServices.begin(), + providedServices.end(), + incompatibleServiceFilter.begin(), + incompatibleServiceFilter.end()) != providedServices.end()) + { + return false; + } + + return true; + } + + bool OffersRequiredServices( + const AZ::SerializeContext::ClassData* componentClass, + const AZStd::vector& serviceFilter + ) + { + const AZStd::vector incompatibleServices; + return OffersRequiredServices(componentClass, serviceFilter, incompatibleServices); + } + bool ShouldInspectorShowComponent(const AZ::Component* component) { if (!component) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.h index 73acb2deb0..c2d693c553 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.h @@ -105,6 +105,16 @@ namespace AzToolsFramework AZ::ComponentDescriptor* GetComponentDescriptor(const AZ::Component* component); Components::EditorComponentDescriptor* GetEditorComponentDescriptor(const AZ::Component* component); Components::EditorComponentBase* GetEditorComponent(AZ::Component* component); + // Returns true if the given component provides at least one of the services specified or no services are provided + bool OffersRequiredServices( + const AZ::SerializeContext::ClassData* componentClass, + const AZStd::vector& serviceFilter, + const AZStd::vector& incompatibleServiceFilter + ); + bool OffersRequiredServices( + const AZ::SerializeContext::ClassData* componentClass, + const AZStd::vector& serviceFilter + ); /// Return true if the editor should show this component to users, /// false if the component should be hidden from users. diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp index 7db507e751..67b5d99011 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp @@ -53,7 +53,7 @@ namespace AzToolsFramework AZ_Assert(m_loaderInterface != nullptr, "Couldn't get prefab loader interface, it's a requirement for PrefabEntityOwnership system to work"); - m_rootInstance = AZStd::unique_ptr(m_prefabSystemComponent->CreatePrefab({}, {}, "NewLevel.prefab")); + m_rootInstance = AZStd::unique_ptr(m_prefabSystemComponent->CreatePrefab({}, {}, "newLevel.prefab")); m_sliceOwnershipService.BusConnect(m_entityContextId); m_sliceOwnershipService.m_shouldAssertForLegacySlicesUsage = m_shouldAssertForLegacySlicesUsage; m_editorSliceOwnershipService.BusConnect(); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp index 18acccf049..574470ad6e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp @@ -244,6 +244,17 @@ namespace AzToolsFramework const auto eventType = event->type(); + if (eventType == QEvent::Type::MouseMove) + { + // clear override cursor when moving outside of the viewport + const auto* mouseEvent = static_cast(event); + if (m_overrideCursor && !m_sourceWidget->geometry().contains(m_sourceWidget->mapFromGlobal(mouseEvent->globalPos()))) + { + qApp->restoreOverrideCursor(); + m_overrideCursor = false; + } + } + // Only accept mouse & key release events that originate from an object that is not our target widget, // as we don't want to erroneously intercept user input meant for another component. if (object != m_sourceWidget && eventType != QEvent::Type::KeyRelease && eventType != QEvent::Type::MouseButtonRelease) @@ -262,7 +273,7 @@ namespace AzToolsFramework if (eventType == QEvent::FocusIn) { const auto globalCursorPosition = QCursor::pos(); - if (m_sourceWidget->geometry().contains(globalCursorPosition)) + if (m_sourceWidget->geometry().contains(m_sourceWidget->mapFromGlobal(globalCursorPosition))) { HandleMouseMoveEvent(globalCursorPosition); } @@ -452,4 +463,32 @@ namespace AzToolsFramework } } } + + static Qt::CursorShape QtCursorFromAzCursor(const ViewportInteraction::CursorStyleOverride cursorStyleOverride) + { + switch (cursorStyleOverride) + { + case ViewportInteraction::CursorStyleOverride::Forbidden: + return Qt::ForbiddenCursor; + default: + return Qt::ArrowCursor; + } + } + + void QtEventToAzInputMapper::SetOverrideCursor(ViewportInteraction::CursorStyleOverride cursorStyleOverride) + { + ClearOverrideCursor(); + + qApp->setOverrideCursor(QtCursorFromAzCursor(cursorStyleOverride)); + m_overrideCursor = true; + } + + void QtEventToAzInputMapper::ClearOverrideCursor() + { + if (m_overrideCursor) + { + qApp->restoreOverrideCursor(); + m_overrideCursor = false; + } + } } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h index 5add24ad84..4cb391e63b 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h @@ -15,10 +15,11 @@ #include #include #include - #include #include +#include + #include #include #include @@ -55,6 +56,9 @@ namespace AzToolsFramework //! like a dolly or rotation, where mouse movement is important but cursor location is not. void SetCursorCaptureEnabled(bool enabled); + void SetOverrideCursor(ViewportInteraction::CursorStyleOverride cursorStyleOverride); + void ClearOverrideCursor(); + // QObject overrides... bool eventFilter(QObject* object, QEvent* event) override; @@ -164,6 +168,8 @@ namespace AzToolsFramework bool m_enabled = true; // Flags whether or not the cursor is being constrained to the source widget (for invisible mouse movement). bool m_capturingCursor = false; + // Flags whether the cursor has been overridden. + bool m_overrideCursor = false; // Our viewport-specific AZ devices. We control their internal input channel states. AZStd::unique_ptr m_mouseDevice; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp index d95a704e84..e9a5bee87a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp @@ -9,6 +9,8 @@ #include #include +#include +#include #include #include #include @@ -38,6 +40,13 @@ namespace AzToolsFramework AZ::Edit::SliceFlags::DontGatherReference); } } + + if (auto behaviorContext = azrtti_cast(context)) + { + behaviorContext->ConstantProperty( + "EditorPrefabComponentTypeId", BehaviorConstant(AZ::Uuid(EditorPrefabComponent::EditorPrefabComponentTypeId))) + ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation); + } } void EditorPrefabComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h index 8873787bd3..aa15b63ac2 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h @@ -16,7 +16,9 @@ namespace AzToolsFramework class EditorPrefabComponent : public AzToolsFramework::Components::EditorComponentBase { public: - AZ_COMPONENT(EditorPrefabComponent, "{756E5F9C-3E08-4F8D-855C-A5AEEFB6FCDD}", EditorComponentBase); + static constexpr const char* const EditorPrefabComponentTypeId = "{756E5F9C-3E08-4F8D-855C-A5AEEFB6FCDD}"; + + AZ_COMPONENT(EditorPrefabComponent, EditorPrefabComponentTypeId, EditorComponentBase); static void Reflect(AZ::ReflectContext* context); static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp index a79b9eb73d..5ba7382831 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace AzToolsFramework::Prefab { @@ -28,7 +29,9 @@ namespace AzToolsFramework::Prefab "Instance Entity Mapper Interface could not be found. " "Check that it is being correctly initialized."); + EditorEntityInfoNotificationBus::Handler::BusConnect(); EditorEntityContextNotificationBus::Handler::BusConnect(); + PrefabPublicNotificationBus::Handler::BusConnect(); AZ::Interface::Register(this); AZ::Interface::Register(this); } @@ -37,10 +40,12 @@ namespace AzToolsFramework::Prefab { AZ::Interface::Unregister(this); AZ::Interface::Unregister(this); + PrefabPublicNotificationBus::Handler::BusDisconnect(); EditorEntityContextNotificationBus::Handler::BusDisconnect(); + EditorEntityInfoNotificationBus::Handler::BusDisconnect(); } - void PrefabFocusHandler::Initialize() + void PrefabFocusHandler::InitializeEditorInterfaces() { m_containerEntityInterface = AZ::Interface::Get(); AZ_Assert( @@ -55,13 +60,6 @@ namespace AzToolsFramework::Prefab "Prefab - PrefabFocusHandler - " "Focus Mode Interface could not be found. " "Check that it is being correctly initialized."); - - m_instanceEntityMapperInterface = AZ::Interface::Get(); - AZ_Assert( - m_instanceEntityMapperInterface, - "Prefab - PrefabFocusHandler - " - "Instance Entity Mapper Interface could not be found. " - "Check that it is being correctly initialized."); } PrefabFocusOperationResult PrefabFocusHandler::FocusOnOwningPrefab(AZ::EntityId entityId) @@ -82,7 +80,7 @@ namespace AzToolsFramework::Prefab auto editUndo = aznew PrefabFocusUndo("Edit Prefab"); editUndo->Capture(entityId); editUndo->SetParent(undoBatch.GetUndoBatch()); - ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::RunRedoSeparately, editUndo); + FocusOnPrefabInstanceOwningEntityId(entityId); } return AZ::Success(); @@ -90,16 +88,14 @@ namespace AzToolsFramework::Prefab PrefabFocusOperationResult PrefabFocusHandler::FocusOnPathIndex([[maybe_unused]] AzFramework::EntityContextId entityContextId, int index) { - if (index < 0 || index >= m_instanceFocusVector.size()) + if (index < 0 || index >= m_instanceFocusHierarchy.size()) { return AZ::Failure(AZStd::string("Prefab Focus Handler: Invalid index on FocusOnPathIndex.")); } - InstanceOptionalReference focusedInstance = m_instanceFocusVector[index]; + InstanceOptionalReference focusedInstance = m_instanceFocusHierarchy[index]; - FocusOnOwningPrefab(focusedInstance->get().GetContainerEntityId()); - - return AZ::Success(); + return FocusOnOwningPrefab(focusedInstance->get().GetContainerEntityId()); } PrefabFocusOperationResult PrefabFocusHandler::FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId) @@ -134,41 +130,37 @@ namespace AzToolsFramework::Prefab return AZ::Failure(AZStd::string("Prefab Focus Handler: invalid instance to focus on.")); } - if (!m_isInitialized) - { - Initialize(); - } - - if (!m_focusedInstance.has_value() || &m_focusedInstance->get() != &focusedInstance->get()) - { - // Close all container entities in the old path - CloseInstanceContainers(m_instanceFocusVector); + // Close all container entities in the old path. + CloseInstanceContainers(m_instanceFocusHierarchy); - m_focusedInstance = focusedInstance; - m_focusedTemplateId = focusedInstance->get().GetTemplateId(); + m_focusedInstance = focusedInstance; + m_focusedTemplateId = focusedInstance->get().GetTemplateId(); - AZ::EntityId containerEntityId; + AZ::EntityId containerEntityId; - if (focusedInstance->get().GetParentInstance() != AZStd::nullopt) - { - containerEntityId = focusedInstance->get().GetContainerEntityId(); - } - else - { - containerEntityId = AZ::EntityId(); - } + if (focusedInstance->get().GetParentInstance() != AZStd::nullopt) + { + containerEntityId = focusedInstance->get().GetContainerEntityId(); + } + else + { + containerEntityId = AZ::EntityId(); + } - // Focus on the descendants of the container entity + // Focus on the descendants of the container entity in the Editor, if the interface is initialized. + if (m_focusModeInterface) + { m_focusModeInterface->SetFocusRoot(containerEntityId); + } - // Refresh path variables - RefreshInstanceFocusList(); + // Refresh path variables. + RefreshInstanceFocusList(); + RefreshInstanceFocusPath(); - // Open all container entities in the new path - OpenInstanceContainers(m_instanceFocusVector); + // Open all container entities in the new path. + OpenInstanceContainers(m_instanceFocusHierarchy); - PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged); - } + PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged); return AZ::Success(); } @@ -213,6 +205,34 @@ namespace AzToolsFramework::Prefab return instance.has_value() && (&instance->get() == &m_focusedInstance->get()); } + bool PrefabFocusHandler::IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const + { + if (!m_focusedInstance.has_value()) + { + // PrefabFocusHandler has not been initialized yet. + return false; + } + + if (!entityId.IsValid()) + { + return false; + } + + InstanceOptionalReference instance = m_instanceEntityMapperInterface->FindOwningInstance(entityId); + + while (instance.has_value()) + { + if (&instance->get() == &m_focusedInstance->get()) + { + return true; + } + + instance = instance->get().GetParentInstance(); + } + + return false; + } + const AZ::IO::Path& PrefabFocusHandler::GetPrefabFocusPath([[maybe_unused]] AzFramework::EntityContextId entityContextId) const { return m_instanceFocusPath; @@ -220,49 +240,124 @@ namespace AzToolsFramework::Prefab const int PrefabFocusHandler::GetPrefabFocusPathLength([[maybe_unused]] AzFramework::EntityContextId entityContextId) const { - return aznumeric_cast(m_instanceFocusVector.size()); + return aznumeric_cast(m_instanceFocusHierarchy.size()); } - void PrefabFocusHandler::OnEntityStreamLoadSuccess() + void PrefabFocusHandler::OnContextReset() { - if (!m_isInitialized) - { - Initialize(); - } - // Clear the old focus vector - m_instanceFocusVector.clear(); + m_instanceFocusHierarchy.clear(); // Focus on the root prefab (AZ::EntityId() will default to it) FocusOnPrefabInstanceOwningEntityId(AZ::EntityId()); } + void PrefabFocusHandler::OnEntityInfoUpdatedName(AZ::EntityId entityId, [[maybe_unused]]const AZStd::string& name) + { + // Determine if the entityId is the container for any of the instances in the vector. + auto result = AZStd::find_if( + m_instanceFocusHierarchy.begin(), m_instanceFocusHierarchy.end(), + [entityId](const InstanceOptionalReference& instance) + { + return (instance->get().GetContainerEntityId() == entityId); + } + ); + + if (result != m_instanceFocusHierarchy.end()) + { + // Refresh the path and notify changes. + RefreshInstanceFocusPath(); + PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged); + } + } + + void PrefabFocusHandler::OnPrefabInstancePropagationEnd() + { + // Refresh the path and notify changes in case propagation updated any container names. + RefreshInstanceFocusPath(); + PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged); + } + + void PrefabFocusHandler::OnPrefabTemplateDirtyFlagUpdated(TemplateId templateId, [[maybe_unused]] bool status) + { + // Determine if the templateId matches any of the instances in the vector. + auto result = AZStd::find_if( + m_instanceFocusHierarchy.begin(), m_instanceFocusHierarchy.end(), + [templateId](const InstanceOptionalReference& instance) + { + return (instance->get().GetTemplateId() == templateId); + } + ); + + if (result != m_instanceFocusHierarchy.end()) + { + // Refresh the path and notify changes. + RefreshInstanceFocusPath(); + PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged); + } + } + void PrefabFocusHandler::RefreshInstanceFocusList() { - m_instanceFocusVector.clear(); - m_instanceFocusPath.clear(); + m_instanceFocusHierarchy.clear(); AZStd::list instanceFocusList; - // Use a support list to easily push front while traversing the prefab hierarchy InstanceOptionalReference currentInstance = m_focusedInstance; while (currentInstance.has_value()) { - instanceFocusList.push_front(currentInstance); + m_instanceFocusHierarchy.emplace_back(currentInstance); currentInstance = currentInstance->get().GetParentInstance(); } - // Populate internals using the support list - for (auto& instance : instanceFocusList) + // Invert the vector, since we need the top instance to be at index 0. + AZStd::reverse(m_instanceFocusHierarchy.begin(), m_instanceFocusHierarchy.end()); + } + + void PrefabFocusHandler::RefreshInstanceFocusPath() + { + auto prefabSystemComponentInterface = AZ::Interface::Get(); + + m_instanceFocusPath.clear(); + + size_t index = 0; + size_t maxIndex = m_instanceFocusHierarchy.size() - 1; + + for (const InstanceOptionalReference& instance : m_instanceFocusHierarchy) { - m_instanceFocusPath.Append(instance->get().GetContainerEntity()->get().GetName()); - m_instanceFocusVector.emplace_back(instance); + AZStd::string prefabName; + + if (index < maxIndex) + { + // Get the filename without the extension (stem). + prefabName = instance->get().GetTemplateSourcePath().Stem().Native(); + } + else + { + // Get the full filename. + prefabName = instance->get().GetTemplateSourcePath().Filename().Native(); + } + + if (prefabSystemComponentInterface->IsTemplateDirty(instance->get().GetTemplateId())) + { + prefabName += "*"; + } + + m_instanceFocusPath.Append(prefabName); + + ++index; } } void PrefabFocusHandler::OpenInstanceContainers(const AZStd::vector& instances) const { + // If this is called outside the Editor, this interface won't be initialized. + if (!m_containerEntityInterface) + { + return; + } + for (const InstanceOptionalReference& instance : instances) { if (instance.has_value()) @@ -274,6 +369,12 @@ namespace AzToolsFramework::Prefab void PrefabFocusHandler::CloseInstanceContainers(const AZStd::vector& instances) const { + // If this is called outside the Editor, this interface won't be initialized. + if (!m_containerEntityInterface) + { + return; + } + for (const InstanceOptionalReference& instance : instances) { if (instance.has_value()) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h index 80b7a6859c..9decaed1ec 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h @@ -11,9 +11,11 @@ #include #include +#include #include #include #include +#include #include namespace AzToolsFramework @@ -30,7 +32,9 @@ namespace AzToolsFramework::Prefab class PrefabFocusHandler final : private PrefabFocusInterface , private PrefabFocusPublicInterface + , private PrefabPublicNotificationBus::Handler , private EditorEntityContextNotificationBus::Handler + , private EditorEntityInfoNotificationBus::Handler { public: AZ_CLASS_ALLOCATOR(PrefabFocusHandler, AZ::SystemAllocator, 0); @@ -38,9 +42,8 @@ namespace AzToolsFramework::Prefab PrefabFocusHandler(); ~PrefabFocusHandler(); - void Initialize(); - // PrefabFocusInterface overrides ... + void InitializeEditorInterfaces() override; PrefabFocusOperationResult FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId) override; TemplateId GetFocusedPrefabTemplateId(AzFramework::EntityContextId entityContextId) const override; InstanceOptionalReference GetFocusedPrefabInstance(AzFramework::EntityContextId entityContextId) const override; @@ -50,29 +53,40 @@ namespace AzToolsFramework::Prefab PrefabFocusOperationResult FocusOnPathIndex(AzFramework::EntityContextId entityContextId, int index) override; AZ::EntityId GetFocusedPrefabContainerEntityId(AzFramework::EntityContextId entityContextId) const override; bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const override; + bool IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const override; const AZ::IO::Path& GetPrefabFocusPath(AzFramework::EntityContextId entityContextId) const override; const int GetPrefabFocusPathLength(AzFramework::EntityContextId entityContextId) const override; // EditorEntityContextNotificationBus overrides ... - void OnEntityStreamLoadSuccess() override; - + void OnContextReset() override; + + // EditorEntityInfoNotificationBus overrides ... + void OnEntityInfoUpdatedName(AZ::EntityId entityId, const AZStd::string& name) override; + + // PrefabPublicNotifications overrides ... + void OnPrefabInstancePropagationEnd() override; + void OnPrefabTemplateDirtyFlagUpdated(TemplateId templateId, bool status) override; + private: PrefabFocusOperationResult FocusOnPrefabInstance(InstanceOptionalReference focusedInstance); void RefreshInstanceFocusList(); + void RefreshInstanceFocusPath(); void OpenInstanceContainers(const AZStd::vector& instances) const; void CloseInstanceContainers(const AZStd::vector& instances) const; + //! The instance the editor is currently focusing on. InstanceOptionalReference m_focusedInstance; + //! The templateId of the focused instance. TemplateId m_focusedTemplateId; - AZStd::vector m_instanceFocusVector; + //! The list of instances going from the root (index 0) to the focused instance. + AZStd::vector m_instanceFocusHierarchy; + //! A path containing the names of the containers in the instance focus hierarchy, separated with a /. AZ::IO::Path m_instanceFocusPath; ContainerEntityInterface* m_containerEntityInterface = nullptr; FocusModeInterface* m_focusModeInterface = nullptr; InstanceEntityMapperInterface* m_instanceEntityMapperInterface = nullptr; - - bool m_isInitialized = false; }; } // namespace AzToolsFramework::Prefab diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusInterface.h index 25c83b89bc..287cbbaf96 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusInterface.h @@ -26,6 +26,11 @@ namespace AzToolsFramework::Prefab public: AZ_RTTI(PrefabFocusInterface, "{F3CFA37B-5FD8-436A-9C30-60EB54E350E1}"); + //! Initializes the editor interfaces for Prefab Focus mode. + //! If this is not called on initialization, the Prefab Focus Mode functions will still work + //! but won't trigger the Editor APIs to visualize focus mode on the UI. + virtual void InitializeEditorInterfaces() = 0; + //! Set the focused prefab instance to the owning instance of the entityId provided. //! @param entityId The entityId of the entity whose owning instance we want the prefab system to focus on. virtual PrefabFocusOperationResult FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId) = 0; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusPublicInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusPublicInterface.h index 86e476b56f..5bd4c6b0f6 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusPublicInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusPublicInterface.h @@ -37,10 +37,15 @@ namespace AzToolsFramework::Prefab //! Returns the entity id of the container entity for the instance the prefab system is focusing on. virtual AZ::EntityId GetFocusedPrefabContainerEntityId(AzFramework::EntityContextId entityContextId) const = 0; + //! Returns whether the entity belongs to the instance that is being focused on. + //! @param entityId The entityId of the queried entity. + //! @return true if the entity belongs to the focused instance, false otherwise. + virtual bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const = 0; + //! Returns whether the entity belongs to the instance that is being focused on, or one of its descendants. //! @param entityId The entityId of the queried entity. //! @return true if the entity belongs to the focused instance or one of its descendants, false otherwise. - virtual bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const = 0; + virtual bool IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const = 0; //! Returns the path from the root instance to the currently focused instance. //! @return A path composed from the names of the container entities for the instance path. diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index ee34b628bb..d976c91c3e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -974,7 +974,7 @@ namespace AzToolsFramework return DeleteFromInstance(entityIds, true); } - PrefabOperationResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) + DuplicatePrefabResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) { if (entityIds.empty()) { @@ -1021,6 +1021,7 @@ namespace AzToolsFramework ScopedUndoBatch undoBatch("Duplicate Entities"); + EntityIdList duplicatedEntityAndInstanceIds; { AZ_PROFILE_SCOPE(AzToolsFramework, "DuplicateEntitiesInInstance::UndoCaptureAndDuplicateEntities"); @@ -1033,7 +1034,7 @@ namespace AzToolsFramework if (!retrieveEntitiesAndInstancesOutcome.IsSuccess()) { - return AZStd::move(retrieveEntitiesAndInstancesOutcome); + return AZ::Failure(retrieveEntitiesAndInstancesOutcome.TakeError()); } // Take a snapshot of the instance DOM before we manipulate it @@ -1044,8 +1045,6 @@ namespace AzToolsFramework PrefabDom instanceDomAfter; instanceDomAfter.CopyFrom(instanceDomBefore, instanceDomAfter.GetAllocator()); - EntityIdList duplicatedEntityAndInstanceIds; - // Duplicate any nested entities and instances as requested AZStd::unordered_map newInstanceAliasToOldInstanceMap; AZStd::unordered_map duplicateEntityAliasMap; @@ -1114,7 +1113,7 @@ namespace AzToolsFramework ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::SetSelectedEntities, duplicatedEntityAndInstanceIds); } - return AZ::Success(); + return AZ::Success(AZStd::move(duplicatedEntityAndInstanceIds)); } PrefabOperationResult PrefabPublicHandler::DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h index a9dadc3336..4961be9d77 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h @@ -63,7 +63,7 @@ namespace AzToolsFramework PrefabOperationResult DeleteEntitiesInInstance(const EntityIdList& entityIds) override; PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override; - PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override; + DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override; PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h index 528d4f6d1b..ede857dd2b 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h @@ -26,6 +26,7 @@ namespace AzToolsFramework { typedef AZ::Outcome CreatePrefabResult; typedef AZ::Outcome InstantiatePrefabResult; + typedef AZ::Outcome DuplicatePrefabResult; typedef AZ::Outcome PrefabOperationResult; typedef AZ::Outcome PrefabRequestResult; typedef AZ::Outcome PrefabEntityResult; @@ -160,14 +161,15 @@ namespace AzToolsFramework /** * Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance. * @param entities The entities to duplicate. - * @return An outcome object; on failure, it comes with an error message detailing the cause of the error. + * @return An outcome object with a list of ids of target entities' duplicates if duplication succeeded; + * on failure, it comes with an error message detailing the cause of the error. */ - virtual PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0; + virtual DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0; /** * If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting * the container entity into a regular entity and putting it under the parent prefab, removing the link between this - * instance and the parent, removing links between this instance and it's nested instances, adding entities directly + * instance and the parent, removing links between this instance and its nested instances, and adding entities directly * owned by this instance under the parent instance. * Bails if the entity is not a container entity or belongs to the level prefab instance. * @param containerEntityId The container entity id of the instance to detach. diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicNotificationBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicNotificationBus.h index 65725b04de..8b0d446f51 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicNotificationBus.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicNotificationBus.h @@ -9,6 +9,7 @@ #pragma once #include +#include namespace AzToolsFramework { @@ -22,6 +23,9 @@ namespace AzToolsFramework virtual void OnPrefabInstancePropagationBegin() {} virtual void OnPrefabInstancePropagationEnd() {} + + virtual void OnPrefabTemplateDirtyFlagUpdated( + [[maybe_unused]] TemplateId templateId, [[maybe_unused]] bool status) {} }; using PrefabPublicNotificationBus = AZ::EBus; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h index 7b23fffb7f..fd4b8a5f17 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h @@ -25,6 +25,7 @@ namespace AzToolsFramework { using CreatePrefabResult = AZ::Outcome; using InstantiatePrefabResult = AZ::Outcome; + using DuplicatePrefabResult = AZ::Outcome; using PrefabOperationResult = AZ::Outcome; /** @@ -69,6 +70,29 @@ namespace AzToolsFramework * Return an outcome object; on failure, it comes with an error message detailing the cause of the error. */ virtual PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) = 0; + + /** + * If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting + * the container entity into a regular entity and putting it under the parent prefab, removing the link between this + * instance and the parent, removing links between this instance and its nested instances, and adding entities directly + * owned by this instance under the parent instance. + * Bails if the entity is not a container entity or belongs to the level prefab instance. + * Return an outcome object; on failure, it comes with an error message detailing the cause of the error. + */ + virtual PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) = 0; + + /** + * Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance. + * Return an outcome object with a list of ids of given entities' duplicates if duplication succeeded; + * on failure, it comes with an error message detailing the cause of the error. + */ + virtual DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0; + + /** + * Get the file path to the prefab file for the prefab instance owning the entity provided. + * Returns the path to the prefab, or an empty path if the entity is owned by the level. + */ + virtual AZStd::string GetOwningInstancePrefabPath(AZ::EntityId entityId) const = 0; }; using PrefabPublicRequestBus = AZ::EBus; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp index 0aaf81c4c9..3b69dcdfe4 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp @@ -28,6 +28,9 @@ namespace AzToolsFramework ->Event("CreatePrefabInMemory", &PrefabPublicRequests::CreatePrefabInMemory) ->Event("InstantiatePrefab", &PrefabPublicRequests::InstantiatePrefab) ->Event("DeleteEntitiesAndAllDescendantsInInstance", &PrefabPublicRequests::DeleteEntitiesAndAllDescendantsInInstance) + ->Event("DetachPrefab", &PrefabPublicRequests::DetachPrefab) + ->Event("DuplicateEntitiesInInstance", &PrefabPublicRequests::DuplicateEntitiesInInstance) + ->Event("GetOwningInstancePrefabPath", &PrefabPublicRequests::GetOwningInstancePrefabPath) ; } } @@ -62,5 +65,19 @@ namespace AzToolsFramework return m_prefabPublicInterface->DeleteEntitiesAndAllDescendantsInInstance(entityIds); } + PrefabOperationResult PrefabPublicRequestHandler::DetachPrefab(const AZ::EntityId& containerEntityId) + { + return m_prefabPublicInterface->DetachPrefab(containerEntityId); + } + + DuplicatePrefabResult PrefabPublicRequestHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) + { + return m_prefabPublicInterface->DuplicateEntitiesInInstance(entityIds); + } + + AZStd::string PrefabPublicRequestHandler::GetOwningInstancePrefabPath(AZ::EntityId entityId) const + { + return m_prefabPublicInterface->GetOwningInstancePrefabPath(entityId).Native(); + } } // namespace Prefab } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h index ae0ed2a5d1..b24ea7ec2a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h @@ -34,6 +34,9 @@ namespace AzToolsFramework CreatePrefabResult CreatePrefabInMemory(const EntityIdList& entityIds, AZStd::string_view filePath) override; InstantiatePrefabResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) override; PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override; + PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override; + DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override; + AZStd::string GetOwningInstancePrefabPath(AZ::EntityId entityId) const override; private: PrefabPublicInterface* m_prefabPublicInterface = nullptr; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.cpp index b1fdf9784d..c930c66786 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabSystemComponent.cpp @@ -185,7 +185,7 @@ namespace AzToolsFramework if (AZ::JsonSerialization::Compare(templateDomToUpdate, updatedDom) != AZ::JsonSerializerCompareResult::Equal) { templateDomToUpdate.CopyFrom(updatedDom, templateDomToUpdate.GetAllocator()); - templateToUpdate->get().MarkAsDirty(true); + SetTemplateDirtyFlag(templateId, true); PropagateTemplateChanges(templateId); } } @@ -813,11 +813,12 @@ namespace AzToolsFramework void PrefabSystemComponent::SetTemplateDirtyFlag(TemplateId templateId, bool dirty) { - auto templateRef = FindTemplate(templateId); - - if (templateRef.has_value()) + if (auto templateReference = FindTemplate(templateId); templateReference.has_value()) { - templateRef->get().MarkAsDirty(dirty); + templateReference->get().MarkAsDirty(dirty); + + PrefabPublicNotificationBus::Broadcast( + &PrefabPublicNotificationBus::Events::OnPrefabTemplateDirtyFlagUpdated, templateId, dirty); } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp index 6c96209d56..1c2230fa83 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp @@ -23,7 +23,7 @@ namespace AzToolsFramework } //PrefabInstanceUndo - PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation) + PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, bool useImmediatePropagation) : PrefabUndoBase(undoOperationName) { m_useImmediatePropagation = useImmediatePropagation; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h index 0946a36951..bc0b86a8c6 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h @@ -45,7 +45,7 @@ namespace AzToolsFramework : public PrefabUndoBase { public: - explicit PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation = true); + explicit PrefabUndoInstance(const AZStd::string& undoOperationName, bool useImmediatePropagation = true); void Capture( const PrefabDom& initialState, diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Template/Template.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Template/Template.cpp index e5ab403ea6..8da926cd08 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Template/Template.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Template/Template.cpp @@ -66,7 +66,16 @@ namespace AzToolsFramework bool Template::IsValid() const { - return !m_prefabDom.IsNull() && !m_filePath.empty(); + if (m_prefabDom.IsNull() || m_filePath.empty()) + { + return false; + } + else if (!m_prefabDom.IsObject()) + { + return false; + } + auto source = m_prefabDom.FindMember(PrefabDomUtils::SourceName); + return (source != m_prefabDom.MemberEnd()); } bool Template::IsLoadedWithErrors() const @@ -175,6 +184,26 @@ namespace AzToolsFramework return findInstancesResult->get(); } + bool Template::IsProcedural() const + { + if (m_isProcedural.has_value()) + { + return m_isProcedural.value(); + } + else if (!IsValid()) + { + return false; + } + auto source = m_prefabDom.FindMember(PrefabDomUtils::SourceName); + if (!source->value.IsString()) + { + return false; + } + AZ::IO::PathView path(source->value.GetString()); + m_isProcedural = AZStd::make_optional(path.Extension().Match(".procprefab")); + return m_isProcedural.value(); + } + const AZ::IO::Path& Template::GetFilePath() const { return m_filePath; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Template/Template.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Template/Template.h index f7a41431f8..9453edfabc 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Template/Template.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Template/Template.h @@ -65,6 +65,9 @@ namespace AzToolsFramework const AZ::IO::Path& GetFilePath() const; void SetFilePath(const AZ::IO::PathView& path); + // To tell if this Template was created from an product asset + bool IsProcedural() const; + private: // Container for keeping links representing the Template's nested instances. Links m_links; @@ -80,6 +83,9 @@ namespace AzToolsFramework // Flag to tell if this Template has changes that have yet to be saved to file. bool m_isDirty = false; + + // Flag to tell if this Template was generated outside the Editor + mutable AZStd::optional m_isProcedural; }; } // namespace Prefab } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Slice/SliceUtilities.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Slice/SliceUtilities.cpp index aca667b9aa..c13f7dd848 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Slice/SliceUtilities.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Slice/SliceUtilities.cpp @@ -2648,16 +2648,15 @@ namespace AzToolsFramework } else { - QString cleanSaveAs(QDir::cleanPath(slicePath)); + AZ::IO::FixedMaxPath lexicallyNormalPath = AZ::IO::PathView(slicePath.toUtf8().constData()).LexicallyNormal(); bool isPathSafeForAssets = false; - for (AZStd::string assetSafeFolder : assetSafeFolders) + for (const AZStd::string& assetSafeFolder : assetSafeFolders) { - QString cleanAssetSafeFolder(QDir::cleanPath(assetSafeFolder.c_str())); - // Compare using clean paths so slash direction does not matter. - // Note that this comparison is case sensitive because some file systems - // Open 3D Engine supports are case sensitive. - if (cleanSaveAs.startsWith(cleanAssetSafeFolder)) + AZ::IO::PathView assetSafeFolderView(assetSafeFolder); + // Check if the slice path is relative to the safe asset directory. + // The Path classes are being used to make this check case insensitive. + if (lexicallyNormalPath.IsRelativeTo(assetSafeFolderView)) { isPathSafeForAssets = true; break; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.cpp index 3bd12b1ad3..6f6f8dccff 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.cpp @@ -20,6 +20,8 @@ #include #include +#include + namespace AzToolsFramework { namespace @@ -75,6 +77,7 @@ namespace AzToolsFramework m_resolveKey = true; m_testTrust = false; + // set up signals before we start thread. m_shutdownThreadSignal = false; m_WorkerThread = AZStd::thread(AZStd::bind(&PerforceComponent::ThreadWorker, this)); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/ComponentPalette/ComponentPaletteUtil.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/ComponentPalette/ComponentPaletteUtil.cpp index 6f3727bdb4..b6f5b41b65 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/ComponentPalette/ComponentPaletteUtil.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/ComponentPalette/ComponentPaletteUtil.cpp @@ -12,6 +12,7 @@ #include #include #include +#include AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option") // 4251: 'QLayoutItem::align': class 'QFlags' needs to have dll-interface to be used by clients of class 'QLayoutItem' #include AZ_POP_DISABLE_WARNING @@ -20,67 +21,6 @@ namespace AzToolsFramework { namespace ComponentPaletteUtil { - bool OffersRequiredServices( - const AZ::SerializeContext::ClassData* componentClass, - const AZStd::vector& serviceFilter, - const AZStd::vector& incompatibleServiceFilter - ) - { - AZ_Assert(componentClass, "Component class must not be null"); - - if (!componentClass) - { - return false; - } - - AZ::ComponentDescriptor* componentDescriptor = nullptr; - EBUS_EVENT_ID_RESULT(componentDescriptor, componentClass->m_typeId, AZ::ComponentDescriptorBus, GetDescriptor); - if (!componentDescriptor) - { - return false; - } - - // If no services are provided, this function returns true - if (serviceFilter.empty()) - { - return true; - } - - AZ::ComponentDescriptor::DependencyArrayType providedServices; - componentDescriptor->GetProvidedServices(providedServices, nullptr); - - //reject this component if it does not offer any of the required services - if (AZStd::find_first_of( - providedServices.begin(), - providedServices.end(), - serviceFilter.begin(), - serviceFilter.end()) == providedServices.end()) - { - return false; - } - - //reject this component if it does offer any of the incompatible services - if (AZStd::find_first_of( - providedServices.begin(), - providedServices.end(), - incompatibleServiceFilter.begin(), - incompatibleServiceFilter.end()) != providedServices.end()) - { - return false; - } - - return true; - } - - bool OffersRequiredServices( - const AZ::SerializeContext::ClassData* componentClass, - const AZStd::vector& serviceFilter - ) - { - const AZStd::vector incompatibleServices; - return OffersRequiredServices(componentClass, serviceFilter, incompatibleServices); - } - bool IsAddableByUser(const AZ::SerializeContext::ClassData* componentClass) { AZ_Assert(componentClass, "component class must not be null"); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/ComponentPalette/ComponentPaletteUtil.hxx b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/ComponentPalette/ComponentPaletteUtil.hxx index 2000d72c5c..dffb893c59 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/ComponentPalette/ComponentPaletteUtil.hxx +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/ComponentPalette/ComponentPaletteUtil.hxx @@ -26,18 +26,6 @@ namespace AzToolsFramework using ComponentIconTable = AZStd::map; - // Returns true if the given component provides at least one of the services specified or no services are provided - bool OffersRequiredServices( - const AZ::SerializeContext::ClassData* componentClass, - const AZStd::vector& serviceFilter, - const AZStd::vector& incompatibleServiceFilter - ); - - bool OffersRequiredServices( - const AZ::SerializeContext::ClassData* componentClass, - const AZStd::vector& serviceFilter - ); - // Returns true if the given component is addable by the user bool IsAddableByUser(const AZ::SerializeContext::ClassData* componentClass); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.cpp index 3cdcade1b0..866080a83f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.cpp @@ -101,8 +101,25 @@ namespace AzToolsFramework { } - void EditorEntityUiHandlerBase::OnDoubleClick([[maybe_unused]] AZ::EntityId entityId) const + bool EditorEntityUiHandlerBase::OnOutlinerItemClick( + [[maybe_unused]] const QPoint& position, + [[maybe_unused]] const QStyleOptionViewItem& option, + [[maybe_unused]] const QModelIndex& index) const + { + return false; + } + + void EditorEntityUiHandlerBase::OnOutlinerItemExpand([[maybe_unused]] const QModelIndex& index) const + { + } + + void EditorEntityUiHandlerBase::OnOutlinerItemCollapse([[maybe_unused]] const QModelIndex& index) const + { + } + + bool EditorEntityUiHandlerBase::OnEntityDoubleClick([[maybe_unused]] AZ::EntityId entityId) const { + return false; } } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.h index 9554ad01fc..96d393efa1 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.h @@ -61,8 +61,17 @@ namespace AzToolsFramework virtual void PaintDescendantForeground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index, const QModelIndex& descendantIndex) const; - //! Triggered when the entity is double clicked in the Outliner. - virtual void OnDoubleClick(AZ::EntityId entityId) const; + //! Triggered when the entity is clicked in the Outliner. + //! @return True if the click has been handled and should not be propagated, false otherwise. + virtual bool OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const; + //! Triggered when an entity's children are expanded in the Outliner. + virtual void OnOutlinerItemExpand(const QModelIndex& index) const; + //! Triggered when an entity's children are collapsed in the Outliner. + virtual void OnOutlinerItemCollapse(const QModelIndex& index) const; + + //! Triggered when the entity is double clicked in the Outliner or in the Viewport. + //! @return True if the double click has been handled and should not be propagated, false otherwise. + virtual bool OnEntityDoubleClick(AZ::EntityId entityId) const; private: EditorEntityUiHandlerId m_handlerId = 0; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastBus.h new file mode 100644 index 0000000000..8b6b4d8ebf --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastBus.h @@ -0,0 +1,91 @@ +/* + * 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 +#include + +#include +#endif + +namespace AzToolsFramework +{ + typedef AZ::EntityId ToastId; + + /** + * An EBus for receiving notifications when a user interacts with or dismisses + * a toast notification. + */ + class ToastNotifications + : public AZ::EBusTraits + { + public: + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById; + using BusIdType = ToastId; + + virtual void OnToastInteraction() {} + virtual void OnToastDismissed() {} + }; + + using ToastNotificationBus = AZ::EBus; + + typedef AZ::u32 ToastRequestBusId; + + /** + * An EBus used to hide or show toast notifications. Generally, these request are handled by a + * ToastNotificationsView that has been created with a specific ToastRequestBusId + * e.g. AZ_CRC("ExampleToastNotificationView") + */ + class ToastRequests + : public AZ::EBusTraits + { + public: + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById; + using BusIdType = ToastRequestBusId; // bus is addressed by CRC of the view name + + /** + * Hide a toast notification widget. + * + * @param toastId The toast notification's ToastId + */ + virtual void HideToastNotification(const ToastId& toastId) = 0; + + /** + * Show a toast notification with the specified toast configuration. When handled by a ToastNotificationsView, + * notifications are queued and presented to the user in sequence. + * + * @param toastConfiguration The toast configuration + * @return a ToastId + */ + virtual ToastId ShowToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration) = 0; + + /** + * Show a toast notification with the specified toast configuration at the current moust cursor location. + * + * @param toastConfiguration The toast configuration + * @return a ToastId + */ + virtual ToastId ShowToastAtCursor(const AzQtComponents::ToastConfiguration& toastConfiguration) = 0; + + /** + * Show a toast notification with the specified toast configuration at the specified location. + * + * @param screenPosition The screen position + * @param anchorPoint The anchorPoint for the toast notification widget + * @param toastConfiguration The toast configuration + * @return a ToastId + */ + virtual ToastId ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const AzQtComponents::ToastConfiguration&) = 0; + }; + + using ToastRequestBus = AZ::EBus; +} diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp new file mode 100644 index 0000000000..e039230783 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp @@ -0,0 +1,190 @@ +/* + * 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 AzToolsFramework +{ + ToastNotificationsView::ToastNotificationsView(QWidget* parent, ToastRequestBusId busId) + : QWidget(parent) + { + ToastRequestBus::Handler::BusConnect(busId); + } + + ToastNotificationsView::~ToastNotificationsView() + { + ToastRequestBus::Handler::BusDisconnect(); + } + + void ToastNotificationsView::OnHide() + { + QWidget::hide(); + + if (m_activeNotification.IsValid()) + { + auto notificationIter = m_notifications.find(m_activeNotification); + if (notificationIter != m_notifications.end()) + { + notificationIter->second->hide(); + } + } + } + + void ToastNotificationsView::UpdateToastPosition() + { + if (m_activeNotification.IsValid()) + { + auto notificationIter = m_notifications.find(m_activeNotification); + if (notificationIter != m_notifications.end()) + { + notificationIter->second->UpdatePosition(GetGlobalPoint(), m_anchorPoint); + } + } + } + + void ToastNotificationsView::OnShow() + { + QWidget::show(); + + if (m_activeNotification.IsValid() || !m_queuedNotifications.empty()) + { + DisplayQueuedNotification(); + } + } + + ToastId ToastNotificationsView::ShowToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration) + { + ToastId toastId = CreateToastNotification(toastConfiguration); + m_queuedNotifications.emplace_back(toastId); + + if (!m_activeNotification.IsValid()) + { + DisplayQueuedNotification(); + } + + return toastId; + } + + ToastId ToastNotificationsView::ShowToastAtCursor(const AzQtComponents::ToastConfiguration& toastConfiguration) + { + ToastId toastId = CreateToastNotification(toastConfiguration); + m_notifications[toastId]->ShowToastAtCursor(); + return toastId; + } + + ToastId ToastNotificationsView::ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const AzQtComponents::ToastConfiguration& toastConfiguration) + { + ToastId toastId = CreateToastNotification(toastConfiguration); + m_notifications[toastId]->ShowToastAtPoint(screenPosition, anchorPoint); + return toastId; + } + + void ToastNotificationsView::HideToastNotification(const ToastId& toastId) + { + auto notificationIter = m_notifications.find(toastId); + if (notificationIter != m_notifications.end()) + { + auto queuedIter = AZStd::find(m_queuedNotifications.begin(), m_queuedNotifications.end(), toastId); + if (queuedIter != m_queuedNotifications.end()) + { + m_queuedNotifications.erase(queuedIter); + } + + notificationIter->second->reject(); + } + } + + ToastId ToastNotificationsView::CreateToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration) + { + AzQtComponents::ToastNotification* notification = aznew AzQtComponents::ToastNotification(parentWidget(), toastConfiguration); + ToastId toastId = AZ::Entity::MakeId(); + m_notifications[toastId] = notification; + + QObject::connect( + m_notifications[toastId], &AzQtComponents::ToastNotification::ToastNotificationHidden, + [toastId]() + { + ToastNotificationBus::Event(toastId, &ToastNotificationBus::Events::OnToastDismissed); + }); + + QObject::connect( + m_notifications[toastId], &AzQtComponents::ToastNotification::ToastNotificationInteraction, + [toastId]() + { + ToastNotificationBus::Event(toastId, &ToastNotificationBus::Events::OnToastInteraction); + }); + + return toastId; + } + + QPoint ToastNotificationsView::GetGlobalPoint() + { + QPoint relativePoint = m_offset; + + AZ_Assert(parentWidget(), "ToastNotificationsView has invalid parent QWidget"); + if (m_anchorPoint.x() == 1.0) + { + relativePoint.setX(parentWidget()->width() - m_offset.x()); + } + if (m_anchorPoint.y() == 1.0) + { + relativePoint.setY(parentWidget()->height() - m_offset.y()); + } + + return parentWidget()->mapToGlobal(relativePoint); + } + + void ToastNotificationsView::DisplayQueuedNotification() + { + AZ_Assert(parentWidget(), "ToastNotificationsView has invalid parent QWidget"); + if (m_queuedNotifications.empty() || !parentWidget()->isVisible() || !isVisible()) + { + return; + } + + ToastId toastId = m_queuedNotifications.front(); + m_queuedNotifications.erase(m_queuedNotifications.begin()); + + auto notificationIter = m_notifications.find(toastId); + if (notificationIter != m_notifications.end()) + { + m_activeNotification = toastId; + + notificationIter->second->ShowToastAtPoint(GetGlobalPoint(), m_anchorPoint); + + QObject::connect( + notificationIter->second, &AzQtComponents::ToastNotification::ToastNotificationHidden, + [&]() + { + m_activeNotification.SetInvalid(); + DisplayQueuedNotification(); + } + ); + } + + // If we didn't actually show something, recurse to avoid things getting stuck in the queue. + if (!m_activeNotification.IsValid()) + { + DisplayQueuedNotification(); + } + } + + void ToastNotificationsView::SetOffset(const QPoint& offset) + { + m_offset = offset; + } + + void ToastNotificationsView::SetAnchorPoint(const QPointF& anchorPoint) + { + m_anchorPoint = anchorPoint; + } +} diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.h new file mode 100644 index 0000000000..e13f129467 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.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 + +#if !defined(Q_MOC_RUN) +#include +#include +#include + +#include +#include +#include +#endif + +namespace AzQtComponents +{ + class ToastNotification; +} + +namespace AzToolsFramework +{ + /** + * \brief A QWidget that displays and manages a queue of toast notifications. + * + * This view must be updated by its parent when the parent widget is show, hidden, moved + * or resized because toast notifications are displayed on top of the parent and are not part + * of the layout, so they must be manually moved. + */ + class ToastNotificationsView final + : public QWidget + , protected ToastRequestBus::Handler + { + Q_OBJECT + public: + ToastNotificationsView(QWidget* parent, ToastRequestBusId busId); + ~ToastNotificationsView() override; + + void HideToastNotification(const ToastId& toastId) override; + + ToastId ShowToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration) override; + ToastId ShowToastAtCursor(const AzQtComponents::ToastConfiguration& toastConfiguration) override; + ToastId ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const AzQtComponents::ToastConfiguration&) override; + + void OnHide(); + void OnShow(); + void UpdateToastPosition(); + + void SetOffset(const QPoint& offset); + void SetAnchorPoint(const QPointF& anchorPoint); + + private: + ToastId CreateToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration); + void DisplayQueuedNotification(); + QPoint GetGlobalPoint(); + + ToastId m_activeNotification; + AZStd::unordered_map m_notifications; + AZStd::vector m_queuedNotifications; + + QPoint m_offset = QPoint(10, 10); + QPointF m_anchorPoint = QPointF(1, 0); + }; +} // AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp index 3f8023c1e3..a5f1e29942 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp @@ -11,10 +11,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -2287,7 +2289,14 @@ namespace AzToolsFramework // Now we setup a Text Document so it can draw the rich text QTextDocument textDoc; textDoc.setDefaultFont(optionV4.font); - textDoc.setDefaultStyleSheet("body {color: white}"); + if (option.state & QStyle::State_Enabled) + { + textDoc.setDefaultStyleSheet("body {color: white}"); + } + else + { + textDoc.setDefaultStyleSheet("body {color: #7C7C7C}"); + } textDoc.setHtml("" + entityNameRichText + ""); painter->translate(textRect.topLeft()); textDoc.setTextWidth(textRect.width()); @@ -2326,6 +2335,23 @@ namespace AzToolsFramework return true; } + if (event->type() == QEvent::MouseButtonPress) + { + AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); + + if (auto editorEntityUiInterface = AZ::Interface::Get(); editorEntityUiInterface != nullptr) + { + auto mouseEvent = static_cast(event); + + auto entityUiHandler = editorEntityUiInterface->GetHandler(entityId); + + if (entityUiHandler && entityUiHandler->OnOutlinerItemClick(mouseEvent->pos(), option, index)) + { + return true; + } + } + } + return QStyledItemDelegate::editorEvent(event, model, option, index); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp index d3138f5139..d94ced392c 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp @@ -73,6 +73,8 @@ namespace AzToolsFramework void EntityOutlinerTreeView::leaveEvent([[maybe_unused]] QEvent* event) { m_mousePosition = QPoint(); + m_currentHoveredIndex = QModelIndex(); + update(); } void EntityOutlinerTreeView::mousePressEvent(QMouseEvent* event) @@ -129,6 +131,11 @@ namespace AzToolsFramework } m_mousePosition = event->pos(); + if (QModelIndex hoveredIndex = indexAt(m_mousePosition); m_currentHoveredIndex != indexAt(m_mousePosition)) + { + m_currentHoveredIndex = hoveredIndex; + update(); + } //process mouse movement as normal, potentially triggering drag and drop QTreeView::mouseMoveEvent(event); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx index 5d76ec6db2..42b42a59b4 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx @@ -90,6 +90,8 @@ namespace AzToolsFramework const QColor m_selectedColor = QColor(255, 255, 255, 45); const QColor m_hoverColor = QColor(255, 255, 255, 30); + QModelIndex m_currentHoveredIndex; + EditorEntityUiInterface* m_editorEntityFrameworkInterface; }; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp index 4d53102edb..3e97f967fc 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp @@ -902,6 +902,7 @@ namespace AzToolsFramework EditorPickModeRequestBus::Broadcast( &EditorPickModeRequests::StopEntityPickMode); + return; } switch (index.column()) @@ -918,18 +919,30 @@ namespace AzToolsFramework { if (AZ::EntityId entityId = GetEntityIdFromIndex(index); auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId)) { - entityUiHandler->OnDoubleClick(entityId); + entityUiHandler->OnEntityDoubleClick(entityId); } } void EntityOutlinerWidget::OnTreeItemExpanded(const QModelIndex& index) { - m_listModel->OnEntityExpanded(GetEntityIdFromIndex(index)); + AZ::EntityId entityId = GetEntityIdFromIndex(index); + if (auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId)) + { + entityUiHandler->OnOutlinerItemExpand(index); + } + + m_listModel->OnEntityExpanded(entityId); } void EntityOutlinerWidget::OnTreeItemCollapsed(const QModelIndex& index) { - m_listModel->OnEntityCollapsed(GetEntityIdFromIndex(index)); + AZ::EntityId entityId = GetEntityIdFromIndex(index); + if (auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId)) + { + entityUiHandler->OnOutlinerItemCollapse(index); + } + + m_listModel->OnEntityCollapsed(entityId); } void EntityOutlinerWidget::OnExpandEntity(const AZ::EntityId& entityId, bool expand) @@ -1163,7 +1176,7 @@ namespace AzToolsFramework { QTimer::singleShot(1, this, [this]() { m_gui->m_objectTree->setUpdatesEnabled(true); - m_gui->m_objectTree->expandToDepth(0); + m_gui->m_objectTree->expand(m_proxyModel->index(0,0)); }); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp index 526912cf29..ae0d18b077 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -135,6 +136,10 @@ namespace AzToolsFramework return; } + // Initialize Editor functionality for the Prefab Focus Handler + auto prefabFocusInterface = AZ::Interface::Get(); + prefabFocusInterface->InitializeEditorInterfaces(); + EditorContextMenuBus::Handler::BusConnect(); EditorEventsBus::Handler::BusConnect(); PrefabInstanceContainerNotificationBus::Handler::BusConnect(); @@ -250,7 +255,7 @@ namespace AzToolsFramework if (s_prefabPublicInterface->IsInstanceContainerEntity(selectedEntity)) { // Edit Prefab - if (prefabWipFeaturesEnabled && !s_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(selectedEntity)) + if (!s_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(selectedEntity)) { QAction* editAction = menu->addAction(QObject::tr("Edit Prefab")); editAction->setToolTip(QObject::tr("Edit the prefab in focus mode.")); @@ -800,16 +805,15 @@ namespace AzToolsFramework } else { - QString cleanSaveAs(QDir::cleanPath(prefabPath)); + AZ::IO::FixedMaxPath lexicallyNormalPath = AZ::IO::PathView(prefabPath.toUtf8().constData()).LexicallyNormal(); bool isPathSafeForAssets = false; - for (AZStd::string assetSafeFolder : assetSafeFolders) + for (const AZStd::string& assetSafeFolder : assetSafeFolders) { - QString cleanAssetSafeFolder(QDir::cleanPath(assetSafeFolder.c_str())); - // Compare using clean paths so slash direction does not matter. - // Note that this comparison is case sensitive because some file systems - // Open 3D Engine supports are case sensitive. - if (cleanSaveAs.startsWith(cleanAssetSafeFolder)) + AZ::IO::PathView assetSafeFolderView(assetSafeFolder); + // Check if the prefabPath is relative to the safe asset directory. + // The Path classes are being used to make this check case insensitive. + if (lexicallyNormalPath.IsRelativeTo(assetSafeFolderView)) { isPathSafeForAssets = true; break; @@ -1154,25 +1158,14 @@ namespace AzToolsFramework { s_editorEntityUiInterface->RegisterEntity(entityId, m_prefabUiHandler.GetHandlerId()); - bool prefabWipFeaturesEnabled = false; - AzFramework::ApplicationRequests::Bus::BroadcastResult( - prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled); - - if (prefabWipFeaturesEnabled) - { - // Register entity as a container - s_containerEntityInterface->RegisterEntityAsContainer(entityId); - } + // Register entity as a container + s_containerEntityInterface->RegisterEntityAsContainer(entityId); } } void PrefabIntegrationManager::OnPrefabComponentDeactivate(AZ::EntityId entityId) { - bool prefabWipFeaturesEnabled = false; - AzFramework::ApplicationRequests::Bus::BroadcastResult( - prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled); - - if (prefabWipFeaturesEnabled && !s_prefabPublicInterface->IsLevelInstanceContainerEntity(entityId)) + if (!s_prefabPublicInterface->IsLevelInstanceContainerEntity(entityId)) { // Unregister entity as a container s_containerEntityInterface->UnregisterEntityAsContainer(entityId); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp index ccad85e32b..447f94fc15 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp @@ -21,10 +21,16 @@ namespace AzToolsFramework { + const QColor PrefabUiHandler::m_backgroundColor = QColor("#444444"); + const QColor PrefabUiHandler::m_backgroundHoverColor = QColor("#5A5A5A"); + const QColor PrefabUiHandler::m_backgroundSelectedColor = QColor("#656565"); const QColor PrefabUiHandler::m_prefabCapsuleColor = QColor("#1E252F"); + const QColor PrefabUiHandler::m_prefabCapsuleDisabledColor = QColor("#35383C"); const QColor PrefabUiHandler::m_prefabCapsuleEditColor = QColor("#4A90E2"); const QString PrefabUiHandler::m_prefabIconPath = QString(":/Entity/prefab.svg"); const QString PrefabUiHandler::m_prefabEditIconPath = QString(":/Entity/prefab_edit.svg"); + const QString PrefabUiHandler::m_prefabEditOpenIconPath = QString(":/Entity/prefab_edit_open.svg"); + const QString PrefabUiHandler::m_prefabEditCloseIconPath = QString(":/Entity/prefab_edit_close.svg"); PrefabUiHandler::PrefabUiHandler() { @@ -75,7 +81,7 @@ namespace AzToolsFramework if (!path.empty()) { - tooltip = QObject::tr("%1").arg(path.Native().data()); + tooltip = QObject::tr("Double click to edit.\n%1").arg(path.Native().data()); } return tooltip; @@ -102,13 +108,20 @@ namespace AzToolsFramework AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); const bool isFirstColumn = index.column() == EntityOutlinerListModel::ColumnName; const bool isLastColumn = index.column() == EntityOutlinerListModel::ColumnLockToggle; - const bool hasVisibleChildren = index.data(EntityOutlinerListModel::ExpandedRole).value() && index.model()->hasChildren(index); + QModelIndex firstColumnIndex = index.siblingAtColumn(EntityOutlinerListModel::ColumnName); + const bool hasVisibleChildren = + firstColumnIndex.data(EntityOutlinerListModel::ExpandedRole).value() && + firstColumnIndex.model()->hasChildren(firstColumnIndex); QColor backgroundColor = m_prefabCapsuleColor; if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) { backgroundColor = m_prefabCapsuleEditColor; } + else if (!(option.state & QStyle::State_Enabled)) + { + backgroundColor = m_prefabCapsuleDisabledColor; + } QPainterPath backgroundPath; backgroundPath.setFillRule(Qt::WindingFill); @@ -184,7 +197,8 @@ namespace AzToolsFramework const bool isFirstColumn = descendantIndex.column() == EntityOutlinerListModel::ColumnName; const bool isLastColumn = descendantIndex.column() == EntityOutlinerListModel::ColumnLockToggle; - QColor borderColor = m_prefabCapsuleColor; + // There is no legal way of opening prefabs in their default state, so default to disabled. + QColor borderColor = m_prefabCapsuleDisabledColor; if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) { borderColor = m_prefabCapsuleEditColor; @@ -273,6 +287,71 @@ namespace AzToolsFramework painter->restore(); } + void PrefabUiHandler::PaintItemForeground(QPainter* painter, const QStyleOptionViewItem& option, [[maybe_unused]] const QModelIndex& index) const + { + AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); + const QPoint offset = QPoint(-18, 3); + QModelIndex firstColumnIndex = index.siblingAtColumn(EntityOutlinerListModel::ColumnName); + const int iconSize = 16; + const bool isHovered = (option.state & QStyle::State_MouseOver); + const bool isSelected = index.data(EntityOutlinerListModel::SelectedRole).template value(); + const bool isFirstColumn = index.column() == EntityOutlinerListModel::ColumnName; + const bool isExpanded = + firstColumnIndex.data(EntityOutlinerListModel::ExpandedRole).value() && + firstColumnIndex.model()->hasChildren(firstColumnIndex); + + if (!isFirstColumn || !(option.state & QStyle::State_Enabled)) + { + return; + } + + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + + if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) + { + // Only show the close icon if the prefab is expanded. + // This allows the prefab container to be opened if it was collapsed during propagation. + if (!isExpanded) + { + return; + } + + // Use the same color as the background. + QColor backgroundColor = m_backgroundColor; + if (isSelected) + { + backgroundColor = m_backgroundSelectedColor; + } + else if (isHovered) + { + backgroundColor = m_backgroundHoverColor; + } + + // Paint a rect to cover up the expander. + QRect rect = QRect(0, 0, 16, 16); + rect.translate(option.rect.topLeft() + offset); + painter->fillRect(rect, backgroundColor); + + // Paint the icon. + QIcon closeIcon = QIcon(m_prefabEditCloseIconPath); + painter->drawPixmap(option.rect.topLeft() + offset, closeIcon.pixmap(iconSize)); + } + else + { + // Only show the edit icon on hover. + if (!isHovered) + { + return; + } + + QIcon openIcon = QIcon(m_prefabEditOpenIconPath); + painter->drawPixmap(option.rect.topLeft() + offset, openIcon.pixmap(iconSize)); + } + + painter->restore(); + } + bool PrefabUiHandler::IsLastVisibleChild(const QModelIndex& parent, const QModelIndex& child) { QModelIndex lastVisibleItemIndex = GetLastVisibleChild(parent); @@ -314,16 +393,53 @@ namespace AzToolsFramework return Internal_GetLastVisibleChild(model, lastChild); } - void PrefabUiHandler::OnDoubleClick(AZ::EntityId entityId) const + bool PrefabUiHandler::OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const + { + AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); + const QPoint offset = QPoint(-18, 3); + + if (m_prefabFocusPublicInterface->IsOwningPrefabInFocusHierarchy(entityId)) + { + QRect iconRect = QRect(0, 0, 16, 16); + iconRect.translate(option.rect.topLeft() + offset); + + if (iconRect.contains(position)) + { + if (!m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) + { + // Focus on this prefab. + m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId); + } + + // Don't propagate event. + return true; + } + } + + return false; + } + + void PrefabUiHandler::OnOutlinerItemCollapse(const QModelIndex& index) const { - bool prefabWipFeaturesEnabled = false; - AzFramework::ApplicationRequests::Bus::BroadcastResult( - prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled); + AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); - if (prefabWipFeaturesEnabled) + if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) { - // Focus on this prefab - m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId); + auto editorEntityContextId = AzFramework::EntityContextId::CreateNull(); + EditorEntityContextRequestBus::BroadcastResult(editorEntityContextId, &EditorEntityContextRequests::GetEditorEntityContextId); + + // Go one level up. + int length = m_prefabFocusPublicInterface->GetPrefabFocusPathLength(editorEntityContextId); + m_prefabFocusPublicInterface->FocusOnPathIndex(editorEntityContextId, length - 2); } } + + bool PrefabUiHandler::OnEntityDoubleClick(AZ::EntityId entityId) const + { + // Focus on this prefab + m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId); + + // Don't propagate event. + return true; + } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.h index 7c68d9fd95..6c78afc5b7 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.h @@ -36,7 +36,10 @@ namespace AzToolsFramework void PaintItemBackground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; void PaintDescendantBackground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index, const QModelIndex& descendantIndex) const override; - void OnDoubleClick(AZ::EntityId entityId) const override; + void PaintItemForeground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + bool OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + void OnOutlinerItemCollapse(const QModelIndex& index) const override; + bool OnEntityDoubleClick(AZ::EntityId entityId) const override; private: Prefab::PrefabFocusPublicInterface* m_prefabFocusPublicInterface = nullptr; @@ -48,9 +51,15 @@ namespace AzToolsFramework static constexpr int m_prefabCapsuleRadius = 6; static constexpr int m_prefabBorderThickness = 2; + static const QColor m_backgroundColor; + static const QColor m_backgroundHoverColor; + static const QColor m_backgroundSelectedColor; static const QColor m_prefabCapsuleColor; + static const QColor m_prefabCapsuleDisabledColor; static const QColor m_prefabCapsuleEditColor; static const QString m_prefabIconPath; static const QString m_prefabEditIconPath; + static const QString m_prefabEditOpenIconPath; + static const QString m_prefabEditCloseIconPath; }; } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabViewportFocusPathHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabViewportFocusPathHandler.cpp index 21ada94184..52cf3279a4 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabViewportFocusPathHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabViewportFocusPathHandler.cpp @@ -10,6 +10,8 @@ #include +#include + namespace AzToolsFramework::Prefab { PrefabViewportFocusPathHandler::PrefabViewportFocusPathHandler() @@ -47,6 +49,9 @@ namespace AzToolsFramework::Prefab [&](const QString&, int linkIndex) { m_prefabFocusPublicInterface->FocusOnPathIndex(m_editorEntityContextId, linkIndex); + + // Manually refresh path + QTimer::singleShot(0, [&]() { OnPrefabFocusChanged(); }); } ); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h index 9a8e0f1f5f..8ec772f0da 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h @@ -302,6 +302,12 @@ namespace AzToolsFramework using EditorViewportInputTimeNowRequestBus = AZ::EBus; + //! The style of cursor override. + enum class CursorStyleOverride + { + Forbidden + }; + //! Viewport requests for managing the viewport cursor state. class ViewportMouseCursorRequests { @@ -312,6 +318,10 @@ namespace AzToolsFramework virtual void EndCursorCapture() = 0; //! Is the mouse over the viewport. virtual bool IsMouseOver() const = 0; + //! Set the cursor style override. + virtual void SetOverrideCursor(CursorStyleOverride cursorStyleOverride) = 0; + //! Clear the cursor style override. + virtual void ClearOverrideCursor() = 0; protected: ~ViewportMouseCursorRequests() = default; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp index 94795c44a9..0f94b95e5f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp @@ -44,6 +44,13 @@ AZ_CVAR( nullptr, AZ::ConsoleFunctorFlags::Null, "Display the aggregate world bounds for a given entity (the union of all world component Aabbs)"); +AZ_CVAR( + bool, + ed_useCursorLockIconInFocusMode, + false, + nullptr, + AZ::ConsoleFunctorFlags::Null, + "Use a lock icon when the cursor is over entities that cannot be interacted with"); namespace AzToolsFramework { @@ -222,6 +229,13 @@ namespace AzToolsFramework // verify if the entity Id corresponds to an entity that is focused; if not, halt selection. if (entityIdUnderCursor.IsValid() && !IsSelectableAccordingToFocusMode(entityIdUnderCursor)) { + if (ed_useCursorLockIconInFocusMode) + { + ViewportInteraction::ViewportMouseCursorRequestBus::Event( + viewportId, &ViewportInteraction::ViewportMouseCursorRequestBus::Events::SetOverrideCursor, + ViewportInteraction::CursorStyleOverride::Forbidden); + } + if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left() && mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down || mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::DoubleClick) @@ -232,6 +246,9 @@ namespace AzToolsFramework return CursorEntityIdQuery(AZ::EntityId(), AZ::EntityId()); } + ViewportInteraction::ViewportMouseCursorRequestBus::Event( + viewportId, &ViewportInteraction::ViewportMouseCursorRequestBus::Events::ClearOverrideCursor); + // container entity support - if the entity that is being selected is part of a closed container, // change the selection to the container instead. if (ContainerEntityInterface* containerEntityInterface = AZ::Interface::Get()) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp index a289d914d6..43e2a6793f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp @@ -20,7 +20,7 @@ namespace AzToolsFramework::ViewportUi::Internal { const static int HighlightBorderSize = 5; const static int TopHighlightBorderSize = 25; - const static char* HighlightBorderColor = "#44B2F8"; + const static char* HighlightBorderColor = "#4A90E2"; static void UnparentWidgets(ViewportUiElementIdInfoLookup& viewportUiElementIdInfoLookup) { diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake b/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake index 5db65f89f4..b57eedcd81 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake @@ -759,6 +759,9 @@ set(FILES UI/Prefab/PrefabUiHandler.cpp UI/Prefab/PrefabViewportFocusPathHandler.h UI/Prefab/PrefabViewportFocusPathHandler.cpp + UI/Notifications/ToastNotificationsView.cpp + UI/Notifications/ToastNotificationsView.h + UI/Notifications/ToastBus.h PythonTerminal/ScriptHelpDialog.cpp PythonTerminal/ScriptHelpDialog.h PythonTerminal/ScriptHelpDialog.ui diff --git a/Code/Framework/AzToolsFramework/Tests/EntityInspectorTests.cpp b/Code/Framework/AzToolsFramework/Tests/EntityInspectorTests.cpp index 0fbc7eba98..d640a91eab 100644 --- a/Code/Framework/AzToolsFramework/Tests/EntityInspectorTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/EntityInspectorTests.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include // Inspector Test Includes @@ -313,17 +314,17 @@ namespace UnitTest AZ_TEST_ASSERT(testComponent1_ProvidedServices.size() == 1); const AZ::SerializeContext::ClassData* testComponent1_ClassData = context->FindClassData(testComponent1_typeId); - EXPECT_TRUE(AzToolsFramework::ComponentPaletteUtil::OffersRequiredServices(testComponent1_ClassData, testComponent1_ProvidedServices)); + EXPECT_TRUE(AzToolsFramework::OffersRequiredServices(testComponent1_ClassData, testComponent1_ProvidedServices)); // Verify that OffersRequiredServices returns when given services provided by a different component AZ::ComponentDescriptor::DependencyArrayType testComponent2_ProvidedServices; Inspector_TestComponent2::GetProvidedServices(testComponent2_ProvidedServices); AZ_TEST_ASSERT(testComponent2_ProvidedServices.size() == 1); AZ_TEST_ASSERT(testComponent1_ProvidedServices != testComponent2_ProvidedServices); - EXPECT_FALSE(AzToolsFramework::ComponentPaletteUtil::OffersRequiredServices(testComponent1_ClassData, testComponent2_ProvidedServices)); + EXPECT_FALSE(AzToolsFramework::OffersRequiredServices(testComponent1_ClassData, testComponent2_ProvidedServices)); // verify that OffersRequiredServices returns true when provided with an empty list of services - EXPECT_TRUE(AzToolsFramework::ComponentPaletteUtil::OffersRequiredServices(testComponent1_ClassData, AZ::ComponentDescriptor::DependencyArrayType())); + EXPECT_TRUE(AzToolsFramework::OffersRequiredServices(testComponent1_ClassData, AZ::ComponentDescriptor::DependencyArrayType())); ////////////////////////////////////////////////////////////////////////// // TEST IsAddableByUser() diff --git a/Code/Framework/AzToolsFramework/Tests/Prefab/ProceduralPrefabAssetTests.cpp b/Code/Framework/AzToolsFramework/Tests/Prefab/ProceduralPrefabAssetTests.cpp index 0b9e73bbac..fb53fa962a 100644 --- a/Code/Framework/AzToolsFramework/Tests/Prefab/ProceduralPrefabAssetTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Prefab/ProceduralPrefabAssetTests.cpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace UnitTest { @@ -132,4 +133,32 @@ namespace UnitTest EXPECT_TRUE(outputValue.HasMember("member")); EXPECT_STREQ(outputValue.FindMember("member")->value.GetString(), "value"); } + + TEST_F(ProceduralPrefabAssetTest, Template_IsProcPrefab_DefaultsToNotProcPrefab) + { + AzToolsFramework::Prefab::PrefabDom dom; + dom.SetObject(); + dom.AddMember("Source", "foo.prefab", dom.GetAllocator()); + AzToolsFramework::Prefab::Template fooTemplate("foo", AZStd::move(dom)); + EXPECT_FALSE(fooTemplate.IsProcedural()); + } + + TEST_F(ProceduralPrefabAssetTest, Template_IsProcPrefab_DomDrivesFlagToTrue) + { + AzToolsFramework::Prefab::PrefabDom dom; + dom.SetObject(); + dom.AddMember("Source", "foo.procprefab", dom.GetAllocator()); + AzToolsFramework::Prefab::Template fooTemplate("foo", AZStd::move(dom)); + EXPECT_TRUE(fooTemplate.IsProcedural()); + // the second time should use the cached version of the flag + EXPECT_TRUE(fooTemplate.IsProcedural()); + } + + TEST_F(ProceduralPrefabAssetTest, Template_IsProcPrefab_FailsWithNoSource) + { + AzToolsFramework::Prefab::PrefabDom dom; + dom.SetObject(); + AzToolsFramework::Prefab::Template fooTemplate("foo", AZStd::move(dom)); + EXPECT_FALSE(fooTemplate.IsProcedural()); + } } diff --git a/Code/Legacy/CrySystem/LevelSystem/LevelSystem.cpp b/Code/Legacy/CrySystem/LevelSystem/LevelSystem.cpp index 49af5080ec..3a2bba64d3 100644 --- a/Code/Legacy/CrySystem/LevelSystem/LevelSystem.cpp +++ b/Code/Legacy/CrySystem/LevelSystem/LevelSystem.cpp @@ -86,7 +86,7 @@ bool CLevelInfo::ReadInfo() usePrefabSystemForLevels, &AzFramework::ApplicationRequests::IsPrefabSystemForLevelsEnabled); // Set up a default game type for legacy code. - m_defaultGameTypeName = "Mission0"; + m_defaultGameTypeName = "mission0"; if (usePrefabSystemForLevels) { @@ -96,17 +96,17 @@ bool CLevelInfo::ReadInfo() AZStd::string levelPath(m_levelPath); AZStd::string xmlFile(levelPath); - xmlFile += "/LevelInfo.xml"; + xmlFile += "/levelinfo.xml"; XmlNodeRef rootNode = GetISystem()->LoadXmlFromFile(xmlFile.c_str()); if (rootNode) { AZStd::string dataFile(levelPath); - dataFile += "/LevelDataAction.xml"; + dataFile += "/leveldataaction.xml"; XmlNodeRef dataNode = GetISystem()->LoadXmlFromFile(dataFile.c_str()); if (!dataNode) { - dataFile = levelPath + "/LevelData.xml"; + dataFile = levelPath + "/leveldata.xml"; dataNode = GetISystem()->LoadXmlFromFile(dataFile.c_str()); } @@ -614,7 +614,7 @@ ILevel* CLevelSystem::LoadLevelInternal(const char* _levelName) } { - AZStd::string missionXml("Mission_"); + AZStd::string missionXml("mission_"); missionXml += pLevelInfo->m_defaultGameTypeName; missionXml += ".xml"; AZStd::string xmlFile(pLevelInfo->GetPath()); diff --git a/Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.cpp b/Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.cpp index ea458ba826..2d3e671999 100644 --- a/Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.cpp +++ b/Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.cpp @@ -72,7 +72,7 @@ struct FolderRootWatch::PlatformImplementation // Add the folder to watch and track it int watchHandle = inotify_add_watch(m_iNotifyHandle, cleanPath.toUtf8().constData(), - IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY); + IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE); if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout)) { @@ -95,7 +95,7 @@ struct FolderRootWatch::PlatformImplementation int watchHandle = inotify_add_watch(m_iNotifyHandle, dirName.toUtf8().constData(), - IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY); + IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE); if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout)) { diff --git a/Code/Tools/AssetProcessor/native/tests/AssetProcessorMessagesTests.cpp b/Code/Tools/AssetProcessor/native/tests/AssetProcessorMessagesTests.cpp index 63536a160b..516f6beb3c 100644 --- a/Code/Tools/AssetProcessor/native/tests/AssetProcessorMessagesTests.cpp +++ b/Code/Tools/AssetProcessor/native/tests/AssetProcessorMessagesTests.cpp @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -98,10 +100,26 @@ namespace AssetProcessorMessagesTests int argC = 0; m_batchApplicationManager = AZStd::make_unique(&argC, nullptr, nullptr); - m_batchApplicationManager->BeforeRun(); - // Override Game Name to be "AutomatedTesting" - AssetUtilities::ComputeProjectName("AutomatedTesting", true); + auto registry = AZ::SettingsRegistry::Get(); + EXPECT_NE(registry, nullptr); + constexpr AZ::SettingsRegistryInterface::FixedValueString bootstrapKey{ + AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey + }; + constexpr AZ::SettingsRegistryInterface::FixedValueString projectPathKey{ bootstrapKey + "/project_path" }; + registry->Set(projectPathKey, "AutomatedTesting"); + AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*registry); + + // Force the branch token into settings registry before starting the application manager. + // This avoids writing the asset_processor.setreg file which can cause fileIO errors. + const AZ::IO::FixedMaxPathString enginePath = AZ::Utils::GetEnginePath(); + constexpr AZ::SettingsRegistryInterface::FixedValueString branchTokenKey{ bootstrapKey + "/assetProcessor_branch_token" }; + AZStd::string token; + AZ::StringFunc::AssetPath::CalculateBranchToken(enginePath.c_str(), token); + registry->Set(branchTokenKey, token.c_str()); + + auto status = m_batchApplicationManager->BeforeRun(); + ASSERT_EQ(status, ApplicationManager::BeforeRunStatus::Status_Success); m_batchApplicationManager->m_platformConfiguration = new PlatformConfiguration(); m_batchApplicationManager->InitAssetProcessorManager(); @@ -159,21 +177,25 @@ namespace AssetProcessorMessagesTests ASSERT_TRUE(result); }); - - } void TearDown() override { - QEventLoop eventLoop; + if (m_batchApplicationManager->m_connectionManager) + { + QEventLoop eventLoop; - QObject::connect(m_batchApplicationManager->m_connectionManager, &ConnectionManager::ReadyToQuit, &eventLoop, &QEventLoop::quit); + QObject::connect(m_batchApplicationManager->m_connectionManager, &ConnectionManager::ReadyToQuit, &eventLoop, &QEventLoop::quit); - m_batchApplicationManager->m_connectionManager->QuitRequested(); + m_batchApplicationManager->m_connectionManager->QuitRequested(); - eventLoop.exec(); + eventLoop.exec(); + } - m_assetSystemComponent->Deactivate(); + if (m_assetSystemComponent) + { + m_assetSystemComponent->Deactivate(); + } m_batchApplicationManager->Destroy(); } diff --git a/Code/Tools/AssetProcessor/native/utilities/ApplicationManager.cpp b/Code/Tools/AssetProcessor/native/utilities/ApplicationManager.cpp index c237f4801e..a024ce6c7a 100644 --- a/Code/Tools/AssetProcessor/native/utilities/ApplicationManager.cpp +++ b/Code/Tools/AssetProcessor/native/utilities/ApplicationManager.cpp @@ -505,6 +505,14 @@ bool ApplicationManager::StartAZFramework() AzFramework::Application::Descriptor appDescriptor; AZ::ComponentApplication::StartupParameters params; + QDir projectPath{ AssetUtilities::ComputeProjectPath() }; + if (!projectPath.exists("project.json")) + { + AZStd::string errorMsg = AZStd::string::format("Path '%s' is not a valid project path.", projectPath.path().toUtf8().constData()); + AssetProcessor::MessageInfoBus::Broadcast(&AssetProcessor::MessageInfoBus::Events::OnErrorMessage, errorMsg.c_str()); + return false; + } + QString projectName = AssetUtilities::ComputeProjectName(); // Prevent loading of gems in the Create method of the ComponentApplication @@ -520,7 +528,6 @@ bool ApplicationManager::StartAZFramework() //Registering all the Components m_frameworkApp.RegisterComponentDescriptor(AzFramework::LogComponent::CreateDescriptor()); - Reflect(); const AzFramework::CommandLine* commandLine = nullptr; diff --git a/Code/Tools/AssetProcessor/native/utilities/GUIApplicationManager.cpp b/Code/Tools/AssetProcessor/native/utilities/GUIApplicationManager.cpp index 40d3bd3caa..c3ff22a39e 100644 --- a/Code/Tools/AssetProcessor/native/utilities/GUIApplicationManager.cpp +++ b/Code/Tools/AssetProcessor/native/utilities/GUIApplicationManager.cpp @@ -95,6 +95,8 @@ GUIApplicationManager::~GUIApplicationManager() ApplicationManager::BeforeRunStatus GUIApplicationManager::BeforeRun() { + AssetProcessor::MessageInfoBus::Handler::BusConnect(); + ApplicationManager::BeforeRunStatus status = ApplicationManagerBase::BeforeRun(); if (status != ApplicationManager::BeforeRunStatus::Status_Success) { @@ -109,7 +111,6 @@ ApplicationManager::BeforeRunStatus GUIApplicationManager::BeforeRun() #if defined(EXTERNAL_CRASH_REPORTING) CrashHandler::ToolsCrashHandler::InitCrashHandler("AssetProcessor", projectAssetRoot.absolutePath().toStdString()); #endif - AssetProcessor::MessageInfoBus::Handler::BusConnect(); // we have to monitor both the cache folder and the database file and restart AP if either of them gets deleted // It is important to note that we are monitoring the parent folder and not the actual cache folder itself since @@ -436,98 +437,7 @@ bool GUIApplicationManager::OnError(const char* /*window*/, const char* message) connection = Qt::QueuedConnection; } - if (m_isCurrentlyLoadingGems) - { - // if something goes wrong during gem initialization, this is a special case and we need to be extra helpful. - const char* userSettingsFile = "_WAF_/user_settings.options"; - const char* defaultSettingsFile = "_WAF_/default_settings.json"; - - QDir engineRoot; - AssetUtilities::ComputeEngineRoot(engineRoot); - - QString settingsPath = engineRoot.absoluteFilePath(userSettingsFile); - QString friendlyErrorMessage; - bool usingDefaults = false; - - if (QFile::exists(settingsPath)) - { - QSettings loader(settingsPath, QSettings::IniFormat); - QVariant settingValue = loader.value("Game Projects/enabled_game_projects"); - QStringList compiledProjects = settingValue.toStringList(); - - if (compiledProjects.isEmpty()) - { - QByteArray byteArray; - QFile jsonFile; - jsonFile.setFileName(engineRoot.absoluteFilePath(defaultSettingsFile)); - jsonFile.open(QIODevice::ReadOnly | QIODevice::Text); - byteArray = jsonFile.readAll(); - jsonFile.close(); - - QJsonObject settingsObject = QJsonDocument::fromJson(byteArray).object(); - QJsonArray projectsArray = settingsObject["Game Projects"].toArray(); - - if (!projectsArray.isEmpty()) - { - auto projectObject = projectsArray[0].toObject(); - QString projects = projectObject["default_value"].toString(); - - if (!projects.isEmpty()) - { - compiledProjects = projects.split(','); - usingDefaults = true; - } - } - } - - for (int i = 0; i < compiledProjects.size(); ++i) - { - compiledProjects[i] = compiledProjects[i].trimmed(); - } - - QString enabledProject = AssetUtilities::ComputeProjectName(); - - if (!compiledProjects.contains(enabledProject)) - { - QString projectSourceLine; - - if (usingDefaults) - { - projectSourceLine = QString("The currently compiled projects according to the defaults in %1 are '%2'").arg(defaultSettingsFile); - } - else - { - projectSourceLine = QString("The currently compiled projects according to %1 are '%2'").arg(userSettingsFile); - } - - projectSourceLine = projectSourceLine.arg(compiledProjects.join(", ")); - friendlyErrorMessage = QString("An error occurred while loading gems.\n" - "The enabled game project is not in the list of compiled projects.\n" - "Please configure the enabled project to be compiled and rebuild or change the enabled project.\n" - "The currently enabled game project (from bootstrap.cfg or /%4 command-line parameter) is '%1'.\n" - "%2\n" - "Full error text:\n" - "%3" - ).arg(enabledProject).arg(projectSourceLine).arg(message).arg(AssetUtilities::ProjectPathOverrideParameter); - } - } - - if (friendlyErrorMessage.isEmpty()) - { - friendlyErrorMessage = QString("An error occurred while loading gems.\n" - "This can happen when new gems are added to a project, but those gems need to be built in order to function.\n" - "This can also happen when switching to a different project, one which uses gems which are not yet built.\n" - "To continue, please build the current project before attempting to run Asset Processor again.\n\n" - "Full error text:\n" - "%1").arg(message); - } - QMetaObject::invokeMethod(this, "ShowMessageBox", connection, Q_ARG(QString, QString("Error")), Q_ARG(QString, friendlyErrorMessage), Q_ARG(bool, true)); - } - else - { - QMetaObject::invokeMethod(this, "ShowMessageBox", connection, Q_ARG(QString, QString("Error")), Q_ARG(QString, QString(message)), Q_ARG(bool, true)); - } - + QMetaObject::invokeMethod(this, "ShowMessageBox", connection, Q_ARG(QString, QString("Error")), Q_ARG(QString, QString(message)), Q_ARG(bool, true)); return true; } diff --git a/Code/Tools/ProjectManager/CMakeLists.txt b/Code/Tools/ProjectManager/CMakeLists.txt index 28c1871616..d34abcbc6c 100644 --- a/Code/Tools/ProjectManager/CMakeLists.txt +++ b/Code/Tools/ProjectManager/CMakeLists.txt @@ -43,7 +43,7 @@ ly_add_target( 3rdParty::pybind11 AZ::AzCore AZ::AzFramework - AZ::AzQtComponents + AZ::AzToolsFramework ) ly_add_target( diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qrc b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc index aeaf9a9248..8dd7e4c9b5 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qrc +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc @@ -40,5 +40,6 @@ Delete.svg Download.svg in_progress.gif + gem.svg diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qss b/Code/Tools/ProjectManager/Resources/ProjectManager.qss index 5f7826dbac..6694168f2b 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qss +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qss @@ -61,6 +61,24 @@ QTabBar::tab:focus { color: #4082eb; } +#ToastNotification { + background-color: black; + border-radius: 20px; + border:1px solid #dddddd; + qproperty-minimumSize: 100px 50px; +} + +#ToastNotification #icon_frame { + border-radius: 4px; + qproperty-minimumSize: 44px 20px; +} + +#ToastNotification #iconLabel { + qproperty-minimumSize: 30px 20px; + qproperty-maximumSize: 30px 20px; + margin-left: 6px; +} + /************** General (Forms) **************/ #formLineEditWidget, @@ -218,6 +236,10 @@ QTabBar::tab:focus { color: #666666; } +#verticalSeparatingLine { + color: #888888; +} + /************** Project Settings **************/ #projectSettings { margin-top:42px; @@ -481,6 +503,34 @@ QProgressBar::chunk { font-weight: 600; } +#gemCatalogMenuButton { + qproperty-flat: true; + max-width:36px; + min-width:36px; + max-height:24px; + min-height:24px; +} + +#GemCatalogCartOverlayGemDownloadHeader { + margin:0; + padding: 0px; + background-color: #333333; +} + +#GemCatalogCartOverlayGemDownloadBG { + margin:0; + padding: 0px; + background-color: #444444; +} + +#gemCatalogMenuButton { + qproperty-flat: true; + max-width:36px; + min-width:36px; + max-height:24px; + min-height:24px; +} + #GemCatalogHeaderLabel { font-size: 12px; color: #FFFFFF; diff --git a/Code/Tools/ProjectManager/Resources/gem.svg b/Code/Tools/ProjectManager/Resources/gem.svg new file mode 100644 index 0000000000..2b688d5db5 --- /dev/null +++ b/Code/Tools/ProjectManager/Resources/gem.svg @@ -0,0 +1,3 @@ + + + diff --git a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp index 84b931cb30..65e01803aa 100644 --- a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp @@ -49,6 +49,8 @@ namespace O3DE::ProjectManager m_stack->addWidget(m_gemCatalogScreen); vLayout->addWidget(m_stack); + connect(m_gemCatalogScreen, &ScreenWidget::ChangeScreenRequest, this, &CreateProjectCtrl::OnChangeScreenRequest); + // When there are multiple project templates present, we re-gather the gems when changing the selected the project template. connect(m_newProjectSettingsScreen, &NewProjectSettingsScreen::OnTemplateSelectionChanged, this, [=](int oldIndex, [[maybe_unused]] int newIndex) { @@ -133,7 +135,7 @@ namespace O3DE::ProjectManager } else { - emit GotoPreviousScreenRequest(); + emit GoToPreviousScreenRequest(); } } diff --git a/Code/Tools/ProjectManager/Source/DownloadController.cpp b/Code/Tools/ProjectManager/Source/DownloadController.cpp new file mode 100644 index 0000000000..224b90299c --- /dev/null +++ b/Code/Tools/ProjectManager/Source/DownloadController.cpp @@ -0,0 +1,98 @@ +/* + * 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 +{ + DownloadController::DownloadController(QWidget* parent) + : QObject() + , m_lastProgress(0) + , m_parent(parent) + { + m_worker = new DownloadWorker(); + m_worker->moveToThread(&m_workerThread); + + connect(&m_workerThread, &QThread::started, m_worker, &DownloadWorker::StartDownload); + connect(m_worker, &DownloadWorker::Done, this, &DownloadController::HandleResults); + connect(m_worker, &DownloadWorker::UpdateProgress, this, &DownloadController::UpdateUIProgress); + connect(this, &DownloadController::StartGemDownload, m_worker, &DownloadWorker::StartDownload); + } + + DownloadController::~DownloadController() + { + connect(&m_workerThread, &QThread::finished, m_worker, &DownloadController::deleteLater); + m_workerThread.requestInterruption(); + m_workerThread.quit(); + m_workerThread.wait(); + } + + void DownloadController::AddGemDownload(const QString& gemName) + { + m_gemNames.push_back(gemName); + if (m_gemNames.size() == 1) + { + m_worker->SetGemToDownload(m_gemNames[0], false); + m_workerThread.start(); + } + } + + void DownloadController::CancelGemDownload(const QString& gemName) + { + auto findResult = AZStd::find(m_gemNames.begin(), m_gemNames.end(), gemName); + + if (findResult != m_gemNames.end()) + { + if (findResult == m_gemNames.begin()) + { + // HandleResults will remove the gem upon cancelling + PythonBindingsInterface::Get()->CancelDownload(); + } + else + { + m_gemNames.erase(findResult); + } + } + } + + void DownloadController::UpdateUIProgress(int progress) + { + m_lastProgress = progress; + emit GemDownloadProgress(progress); + } + + void DownloadController::HandleResults(const QString& result) + { + bool succeeded = true; + + if (!result.isEmpty()) + { + QMessageBox::critical(nullptr, tr("Gem download"), result); + succeeded = false; + } + + m_gemNames.erase(m_gemNames.begin()); + emit Done(succeeded); + + if (!m_gemNames.empty()) + { + emit StartGemDownload(m_gemNames[0]); + } + else + { + m_workerThread.quit(); + m_workerThread.wait(); + } + } +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/DownloadController.h b/Code/Tools/ProjectManager/Source/DownloadController.h new file mode 100644 index 0000000000..11ceaacddb --- /dev/null +++ b/Code/Tools/ProjectManager/Source/DownloadController.h @@ -0,0 +1,72 @@ +/* + * 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 +#include +#endif + +QT_FORWARD_DECLARE_CLASS(QProcess) + +namespace O3DE::ProjectManager +{ + QT_FORWARD_DECLARE_CLASS(DownloadWorker) + + class DownloadController : public QObject + { + Q_OBJECT + + public: + explicit DownloadController(QWidget* parent = nullptr); + ~DownloadController(); + + void AddGemDownload(const QString& gemName); + void CancelGemDownload(const QString& gemName); + + bool IsDownloadQueueEmpty() + { + return m_gemNames.empty(); + } + + const AZStd::vector& GetDownloadQueue() const + { + return m_gemNames; + } + + const QString& GetCurrentDownloadingGem() const + { + if (!m_gemNames.empty()) + { + return m_gemNames[0]; + } + else + { + static const QString emptyString; + return emptyString; + } + } + public slots: + void UpdateUIProgress(int progress); + void HandleResults(const QString& result); + + signals: + void StartGemDownload(const QString& gemName); + void Done(bool success = true); + void GemDownloadProgress(int percentage); + + private: + DownloadWorker* m_worker; + QThread m_workerThread; + QWidget* m_parent; + AZStd::vector m_gemNames; + + int m_lastProgress; + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/DownloadWorker.cpp b/Code/Tools/ProjectManager/Source/DownloadWorker.cpp new file mode 100644 index 0000000000..9bda1b34cc --- /dev/null +++ b/Code/Tools/ProjectManager/Source/DownloadWorker.cpp @@ -0,0 +1,48 @@ +/* + * 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 O3DE::ProjectManager +{ + DownloadWorker::DownloadWorker() + : QObject() + { + } + + void DownloadWorker::StartDownload() + { + auto gemDownloadProgress = [=](int downloadProgress) + { + m_downloadProgress = downloadProgress; + emit UpdateProgress(downloadProgress); + }; + AZ::Outcome gemInfoResult = PythonBindingsInterface::Get()->DownloadGem(m_gemName, gemDownloadProgress); + if (gemInfoResult.IsSuccess()) + { + emit Done(""); + } + else + { + emit Done(tr("Gem download failed")); + } + } + + void DownloadWorker::SetGemToDownload(const QString& gemName, bool downloadNow) + { + m_gemName = gemName; + if (downloadNow) + { + StartDownload(); + } + } + +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/DownloadWorker.h b/Code/Tools/ProjectManager/Source/DownloadWorker.h new file mode 100644 index 0000000000..316a730a78 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/DownloadWorker.h @@ -0,0 +1,42 @@ +/* + * 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(QProcess) + +namespace O3DE::ProjectManager +{ + class DownloadWorker : public QObject + { + // Download was cancelled + inline static const QString DownloadCancelled = QObject::tr("Download Cancelled."); + + Q_OBJECT + + public: + explicit DownloadWorker(); + ~DownloadWorker() = default; + + public slots: + void StartDownload(); + void SetGemToDownload(const QString& gemName, bool downloadNow = true); + + signals: + void UpdateProgress(int progress); + void Done(QString result = ""); + + private: + + QString m_gemName; + int m_downloadProgress; + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp index c78a9426db..f30a8e0daa 100644 --- a/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp @@ -29,17 +29,17 @@ namespace O3DE::ProjectManager topBarFrameWidget->setLayout(topBarHLayout); - QTabWidget* tabWidget = new QTabWidget(); - tabWidget->setObjectName("engineTab"); - tabWidget->tabBar()->setObjectName("engineTabBar"); - tabWidget->tabBar()->setFocusPolicy(Qt::TabFocus); + m_tabWidget = new QTabWidget(); + m_tabWidget->setObjectName("engineTab"); + m_tabWidget->tabBar()->setObjectName("engineTabBar"); + m_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); + m_tabWidget->addTab(m_engineSettingsScreen, tr("General")); + m_tabWidget->addTab(m_gemRepoScreen, tr("Gem Repositories")); + topBarHLayout->addWidget(m_tabWidget); vLayout->addWidget(topBarFrameWidget); @@ -61,4 +61,28 @@ namespace O3DE::ProjectManager return true; } + bool EngineScreenCtrl::ContainsScreen(ProjectManagerScreen screen) + { + if (screen == m_engineSettingsScreen->GetScreenEnum() || screen == m_gemRepoScreen->GetScreenEnum()) + { + return true; + } + + return false; + } + + void EngineScreenCtrl::GoToScreen(ProjectManagerScreen screen) + { + if (screen == m_engineSettingsScreen->GetScreenEnum()) + { + m_tabWidget->setCurrentWidget(m_engineSettingsScreen); + m_engineSettingsScreen->NotifyCurrentScreen(); + } + else if (screen == m_gemRepoScreen->GetScreenEnum()) + { + m_tabWidget->setCurrentWidget(m_gemRepoScreen); + m_gemRepoScreen->NotifyCurrentScreen(); + } + } + } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h index 9e799f13e7..b7142ba226 100644 --- a/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h +++ b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h @@ -11,6 +11,8 @@ #include #endif +QT_FORWARD_DECLARE_CLASS(QTabWidget) + namespace O3DE::ProjectManager { QT_FORWARD_DECLARE_CLASS(EngineSettingsScreen) @@ -26,7 +28,10 @@ namespace O3DE::ProjectManager QString GetTabText() override; bool IsTab() override; + bool ContainsScreen(ProjectManagerScreen screen) override; + void GoToScreen(ProjectManagerScreen screen) override; + QTabWidget* m_tabWidget = nullptr; EngineSettingsScreen* m_engineSettingsScreen = nullptr; GemRepoScreen* m_gemRepoScreen = nullptr; }; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp index 9fca6040d4..5d65c740af 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp @@ -12,13 +12,16 @@ #include #include #include +#include #include +#include namespace O3DE::ProjectManager { - CartOverlayWidget::CartOverlayWidget(GemModel* gemModel, QWidget* parent) + CartOverlayWidget::CartOverlayWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent) : QWidget(parent) , m_gemModel(gemModel) + , m_downloadController(downloadController) { setObjectName("GemCatalogCart"); @@ -42,6 +45,9 @@ namespace O3DE::ProjectManager hLayout->addWidget(closeButton); m_layout->addLayout(hLayout); + // downloading gems + CreateDownloadSection(); + // added CreateGemSection( tr("Gem to be activated"), tr("Gems to be activated"), [=] { @@ -149,6 +155,116 @@ namespace O3DE::ProjectManager update(); } + void CartOverlayWidget::OnCancelDownloadActivated(const QString& gemName) + { + m_downloadController->CancelGemDownload(gemName); + } + + void CartOverlayWidget::CreateDownloadSection() + { + QWidget* widget = new QWidget(); + widget->setFixedWidth(s_width); + m_layout->addWidget(widget); + + QVBoxLayout* layout = new QVBoxLayout(); + layout->setAlignment(Qt::AlignTop); + widget->setLayout(layout); + + QLabel* titleLabel = new QLabel(); + titleLabel->setObjectName("GemCatalogCartOverlaySectionLabel"); + layout->addWidget(titleLabel); + + titleLabel->setText(tr("Gems to be installed")); + + // Create header section + QWidget* downloadingGemsWidget = new QWidget(); + downloadingGemsWidget->setObjectName("GemCatalogCartOverlayGemDownloadHeader"); + layout->addWidget(downloadingGemsWidget); + QVBoxLayout* gemDownloadLayout = new QVBoxLayout(); + gemDownloadLayout->setMargin(0); + gemDownloadLayout->setAlignment(Qt::AlignTop); + downloadingGemsWidget->setLayout(gemDownloadLayout); + QLabel* processingQueueLabel = new QLabel("Processing Queue"); + gemDownloadLayout->addWidget(processingQueueLabel); + + QWidget* downloadingItemWidget = new QWidget(); + downloadingItemWidget->setObjectName("GemCatalogCartOverlayGemDownloadBG"); + gemDownloadLayout->addWidget(downloadingItemWidget); + QVBoxLayout* downloadingItemLayout = new QVBoxLayout(); + downloadingItemLayout->setAlignment(Qt::AlignTop); + downloadingItemWidget->setLayout(downloadingItemLayout); + + auto update = [=](int downloadProgress) + { + if (m_downloadController->IsDownloadQueueEmpty()) + { + widget->hide(); + } + else + { + widget->setUpdatesEnabled(false); + // remove items + QLayoutItem* layoutItem = nullptr; + while ((layoutItem = downloadingItemLayout->takeAt(0)) != nullptr) + { + if (layoutItem->layout()) + { + // Gem info row + QLayoutItem* rowLayoutItem = nullptr; + while ((rowLayoutItem = layoutItem->layout()->takeAt(0)) != nullptr) + { + rowLayoutItem->widget()->deleteLater(); + } + layoutItem->layout()->deleteLater(); + } + if (layoutItem->widget()) + { + layoutItem->widget()->deleteLater(); + } + } + + // Setup gem download rows + const AZStd::vector& downloadQueue = m_downloadController->GetDownloadQueue(); + + QLabel* downloadsInProgessLabel = new QLabel(""); + downloadsInProgessLabel->setText( + QString("%1 %2").arg(downloadQueue.size()).arg(downloadQueue.size() == 1 ? tr("download in progress...") : tr("downloads in progress..."))); + downloadingItemLayout->addWidget(downloadsInProgessLabel); + + for (int downloadingGemNumber = 0; downloadingGemNumber < downloadQueue.size(); ++downloadingGemNumber) + { + QHBoxLayout* nameProgressLayout = new QHBoxLayout(); + TagWidget* newTag = new TagWidget(downloadQueue[downloadingGemNumber]); + nameProgressLayout->addWidget(newTag); + QLabel* progress = new QLabel(downloadingGemNumber == 0? QString("%1%").arg(downloadProgress) : tr("Queued")); + nameProgressLayout->addWidget(progress); + QSpacerItem* spacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum); + nameProgressLayout->addSpacerItem(spacer); + QLabel* cancelText = new QLabel(QString("Cancel").arg(downloadQueue[downloadingGemNumber])); + cancelText->setTextInteractionFlags(Qt::LinksAccessibleByMouse); + connect(cancelText, &QLabel::linkActivated, this, &CartOverlayWidget::OnCancelDownloadActivated); + nameProgressLayout->addWidget(cancelText); + downloadingItemLayout->addLayout(nameProgressLayout); + QProgressBar* downloadProgessBar = new QProgressBar(); + downloadingItemLayout->addWidget(downloadProgessBar); + downloadProgessBar->setValue(downloadingGemNumber == 0 ? downloadProgress : 0); + } + + widget->setUpdatesEnabled(true); + widget->show(); + } + }; + + auto downloadEnded = [=](bool /*success*/) + { + update(0); // update the list to remove the gem that has finished + }; + // connect to download controller data changed + connect(m_downloadController, &DownloadController::GemDownloadProgress, this, update); + connect(m_downloadController, &DownloadController::Done, this, downloadEnded); + update(0); + } + QStringList CartOverlayWidget::ConvertFromModelIndices(const QVector& gems) const { QStringList gemNames; @@ -160,9 +276,10 @@ namespace O3DE::ProjectManager return gemNames; } - CartButton::CartButton(GemModel* gemModel, QWidget* parent) + CartButton::CartButton(GemModel* gemModel, DownloadController* downloadController, QWidget* parent) : QWidget(parent) , m_gemModel(gemModel) + , m_downloadController(downloadController) { m_layout = new QHBoxLayout(); m_layout->setMargin(0); @@ -239,7 +356,7 @@ namespace O3DE::ProjectManager delete m_cartOverlay; } - m_cartOverlay = new CartOverlayWidget(m_gemModel, this); + m_cartOverlay = new CartOverlayWidget(m_gemModel, m_downloadController, this); connect(m_cartOverlay, &QWidget::destroyed, this, [=] { // Reset the overlay pointer on destruction to prevent dangling pointers. @@ -265,7 +382,7 @@ namespace O3DE::ProjectManager } } - GemCatalogHeaderWidget::GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, QWidget* parent) + GemCatalogHeaderWidget::GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, DownloadController* downloadController, QWidget* parent) : QFrame(parent) { QHBoxLayout* hLayout = new QHBoxLayout(); @@ -293,8 +410,29 @@ namespace O3DE::ProjectManager hLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding)); hLayout->addSpacerItem(new QSpacerItem(75, 0, QSizePolicy::Fixed)); - CartButton* cartButton = new CartButton(gemModel); + CartButton* cartButton = new CartButton(gemModel, downloadController); hLayout->addWidget(cartButton); + hLayout->addSpacing(16); + + // Separating line + QFrame* vLine = new QFrame(); + vLine->setFrameShape(QFrame::VLine); + vLine->setObjectName("verticalSeparatingLine"); + hLayout->addWidget(vLine); + + hLayout->addSpacing(16); + + QMenu* gemMenu = new QMenu(this); + gemMenu->addAction( tr("Show Gem Repos"), [this]() { emit OpenGemsRepo(); }); + gemMenu->addSeparator(); + gemMenu->addAction( tr("Add Existing Gem"), [this]() { emit AddGem(); }); + + QPushButton* gemMenuButton = new QPushButton(this); + gemMenuButton->setObjectName("gemCatalogMenuButton"); + gemMenuButton->setMenu(gemMenu); + gemMenuButton->setIcon(QIcon(":/menu.svg")); + gemMenuButton->setIconSize(QSize(36, 24)); + hLayout->addWidget(gemMenuButton); } void GemCatalogHeaderWidget::ReinitForProject() diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h index 2cfda4c790..4d17259840 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h @@ -8,21 +8,23 @@ #pragma once -#include - #if !defined(Q_MOC_RUN) +#include #include #include #include #include #include -#include -#include -#include -#include -#include +#include #endif +QT_FORWARD_DECLARE_CLASS(QPushButton) +QT_FORWARD_DECLARE_CLASS(QLabel) +QT_FORWARD_DECLARE_CLASS(QVBoxLayout) +QT_FORWARD_DECLARE_CLASS(QHBoxLayout) +QT_FORWARD_DECLARE_CLASS(QHideEvent) +QT_FORWARD_DECLARE_CLASS(QMoveEvent) + namespace O3DE::ProjectManager { class CartOverlayWidget @@ -31,16 +33,19 @@ namespace O3DE::ProjectManager Q_OBJECT // AUTOMOC public: - CartOverlayWidget(GemModel* gemModel, QWidget* parent = nullptr); + CartOverlayWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr); private: QStringList ConvertFromModelIndices(const QVector& gems) const; using GetTagIndicesCallback = AZStd::function()>; void CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices); + void CreateDownloadSection(); + void OnCancelDownloadActivated(const QString& link); QVBoxLayout* m_layout = nullptr; GemModel* m_gemModel = nullptr; + DownloadController* m_downloadController = nullptr; inline constexpr static int s_width = 240; }; @@ -51,7 +56,7 @@ namespace O3DE::ProjectManager Q_OBJECT // AUTOMOC public: - CartButton(GemModel* gemModel, QWidget* parent = nullptr); + CartButton(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr); ~CartButton(); void ShowOverlay(); @@ -64,6 +69,7 @@ namespace O3DE::ProjectManager QLabel* m_countLabel = nullptr; QPushButton* m_dropDownButton = nullptr; CartOverlayWidget* m_cartOverlay = nullptr; + DownloadController* m_downloadController = nullptr; inline constexpr static int s_iconSize = 24; inline constexpr static int s_arrowDownIconSize = 8; @@ -75,11 +81,15 @@ namespace O3DE::ProjectManager Q_OBJECT // AUTOMOC public: - explicit GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, QWidget* parent = nullptr); + explicit GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, DownloadController* downloadController, QWidget* parent = nullptr); ~GemCatalogHeaderWidget() = default; void ReinitForProject(); + signals: + void AddGem(); + void OpenGemsRepo(); + private: AzQtComponents::SearchLineEdit* m_filterLineEdit = nullptr; inline constexpr static int s_height = 60; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp index 945878768d..b145363460 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp @@ -12,12 +12,17 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include +#include +#include namespace O3DE::ProjectManager { @@ -32,9 +37,13 @@ namespace O3DE::ProjectManager vLayout->setSpacing(0); setLayout(vLayout); - m_headerWidget = new GemCatalogHeaderWidget(m_gemModel, m_proxModel); + m_downloadController = new DownloadController(); + + m_headerWidget = new GemCatalogHeaderWidget(m_gemModel, m_proxModel, m_downloadController); vLayout->addWidget(m_headerWidget); + connect(m_headerWidget, &GemCatalogHeaderWidget::OpenGemsRepo, this, &GemCatalogScreen::HandleOpenGemRepo); + QHBoxLayout* hLayout = new QHBoxLayout(); hLayout->setMargin(0); vLayout->addLayout(hLayout); @@ -61,11 +70,15 @@ namespace O3DE::ProjectManager hLayout->addWidget(filterWidget); hLayout->addLayout(middleVLayout); hLayout->addWidget(m_gemInspector); + + m_notificationsView = AZStd::make_unique(this, AZ_CRC("GemCatalogNotificationsView")); + m_notificationsView->SetOffset(QPoint(10, 70)); } void GemCatalogScreen::ReinitForProject(const QString& projectPath) { m_gemModel->clear(); + m_gemsToRegisterWithProject.clear(); FillModel(projectPath); if (m_filterWidget) @@ -81,6 +94,48 @@ namespace O3DE::ProjectManager m_headerWidget->ReinitForProject(); connect(m_gemModel, &GemModel::dataChanged, m_filterWidget, &GemFilterWidget::ResetGemStatusFilter); + connect(m_gemModel, &GemModel::gemStatusChanged, this, &GemCatalogScreen::OnGemStatusChanged); + connect( + m_headerWidget, &GemCatalogHeaderWidget::AddGem, + [&]() + { + EngineInfo engineInfo; + QString defaultPath; + + AZ::Outcome engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo(); + if (engineInfoResult.IsSuccess()) + { + engineInfo = engineInfoResult.GetValue(); + defaultPath = engineInfo.m_defaultGemsFolder; + } + + if (defaultPath.isEmpty()) + { + defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + } + + QString directory = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(this, tr("Browse"), defaultPath)); + if (!directory.isEmpty()) + { + // register the gem to the o3de_manifest.json and to the project after the user confirms + // project creation/update + auto registerResult = PythonBindingsInterface::Get()->RegisterGem(directory); + if(!registerResult) + { + QMessageBox::critical(this, tr("Failed to add gem"), registerResult.GetError().c_str()); + } + else + { + m_gemsToRegisterWithProject.insert(directory); + AZ::Outcome gemInfoResult = PythonBindingsInterface::Get()->GetGemInfo(directory); + if (gemInfoResult) + { + m_gemModel->AddGem(gemInfoResult.GetValue()); + m_gemModel->UpdateGemDependencies(); + } + } + } + }); // Select the first entry after everything got correctly sized QTimer::singleShot(200, [=]{ @@ -89,6 +144,72 @@ namespace O3DE::ProjectManager }); } + void GemCatalogScreen::OnGemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies) + { + if (m_notificationsEnabled) + { + bool added = GemModel::IsAdded(modelIndex); + bool dependency = GemModel::IsAddedDependency(modelIndex); + + bool gemStateChanged = (added && !dependency) || (!added && !dependency); + if (!gemStateChanged && !numChangedDependencies) + { + // no actual changes made + return; + } + + QString notification; + if (gemStateChanged) + { + notification = GemModel::GetDisplayName(modelIndex); + if (numChangedDependencies > 0) + { + notification += " " + tr("and") + " "; + } + } + + if (numChangedDependencies == 1 ) + { + notification += "1 Gem " + tr("dependency"); + } + else if (numChangedDependencies > 1) + { + notification += QString("%d Gem ").arg(numChangedDependencies) + tr("dependencies"); + } + notification += " " + (added ? tr("activated") : tr("deactivated")); + + AzQtComponents::ToastConfiguration toastConfiguration(AzQtComponents::ToastType::Custom, notification, ""); + toastConfiguration.m_customIconImage = ":/gem.svg"; + toastConfiguration.m_borderRadius = 4; + toastConfiguration.m_duration = AZStd::chrono::milliseconds(3000); + m_notificationsView->ShowToastNotification(toastConfiguration); + } + } + + void GemCatalogScreen::hideEvent(QHideEvent* event) + { + ScreenWidget::hideEvent(event); + m_notificationsView->OnHide(); + } + + void GemCatalogScreen::showEvent(QShowEvent* event) + { + ScreenWidget::showEvent(event); + m_notificationsView->OnShow(); + } + + void GemCatalogScreen::resizeEvent(QResizeEvent* event) + { + ScreenWidget::resizeEvent(event); + m_notificationsView->UpdateToastPosition(); + } + + void GemCatalogScreen::moveEvent(QMoveEvent* event) + { + ScreenWidget::moveEvent(event); + m_notificationsView->UpdateToastPosition(); + } + void GemCatalogScreen::FillModel(const QString& projectPath) { AZ::Outcome, AZStd::string> allGemInfosResult = PythonBindingsInterface::Get()->GetAllGemInfos(projectPath); @@ -102,6 +223,7 @@ namespace O3DE::ProjectManager } m_gemModel->UpdateGemDependencies(); + m_notificationsEnabled = false; // Gather enabled gems for the given project. auto enabledGemNamesResult = PythonBindingsInterface::Get()->GetEnabledGemNames(projectPath); @@ -128,6 +250,8 @@ namespace O3DE::ProjectManager { QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve enabled gems for project %1.\n\nError:\n%2").arg(projectPath, enabledGemNamesResult.GetError().c_str())); } + + m_notificationsEnabled = true; } else { @@ -173,6 +297,12 @@ namespace O3DE::ProjectManager return EnableDisableGemsResult::Failed; } + + // register external gems that were added with relative paths + if (m_gemsToRegisterWithProject.contains(gemPath)) + { + pythonBindings->RegisterGem(QDir(projectPath).relativeFilePath(gemPath), projectPath); + } } for (const QModelIndex& modelIndex : toBeRemoved) @@ -191,6 +321,27 @@ namespace O3DE::ProjectManager return EnableDisableGemsResult::Success; } + void GemCatalogScreen::HandleOpenGemRepo() + { + QVector gemsToBeAdded = m_gemModel->GatherGemsToBeAdded(true); + QVector gemsToBeRemoved = m_gemModel->GatherGemsToBeRemoved(true); + + if (!gemsToBeAdded.empty() || !gemsToBeRemoved.empty()) + { + QMessageBox::StandardButton warningResult = QMessageBox::warning( + nullptr, "Pending Changes", + "There are some unsaved changes to the gem selection,
they will be lost if you change screens.
Are you sure?", + QMessageBox::No | QMessageBox::Yes); + + if (warningResult != QMessageBox::Yes) + { + return; + } + } + + emit ChangeScreenRequest(ProjectManagerScreen::GemRepos); + } + ProjectManagerScreen GemCatalogScreen::GetScreenEnum() { return ProjectManagerScreen::GemCatalog; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h index 72e8d44f65..8e9f31c710 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h @@ -10,12 +10,16 @@ #if !defined(Q_MOC_RUN) #include +#include +#include #include #include #include #include #include #include +#include +#include #endif namespace O3DE::ProjectManager @@ -39,10 +43,26 @@ namespace O3DE::ProjectManager EnableDisableGemsResult EnableDisableGemsForProject(const QString& projectPath); GemModel* GetGemModel() const { return m_gemModel; } + DownloadController* GetDownloadController() const { return m_downloadController; } + + public slots: + void OnGemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies); + + protected: + void hideEvent(QHideEvent* event) override; + void showEvent(QShowEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + void moveEvent(QMoveEvent* event) override; + + private slots: + void HandleOpenGemRepo(); + private: void FillModel(const QString& projectPath); + AZStd::unique_ptr m_notificationsView; + GemListView* m_gemListView = nullptr; GemInspector* m_gemInspector = nullptr; GemModel* m_gemModel = nullptr; @@ -50,5 +70,8 @@ namespace O3DE::ProjectManager GemSortFilterProxyModel* m_proxModel = nullptr; QVBoxLayout* m_filterWidgetLayout = nullptr; GemFilterWidget* m_filterWidget = nullptr; + DownloadController* m_downloadController = nullptr; + bool m_notificationsEnabled = true; + QSet m_gemsToRegisterWithProject; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp index 35491f4ddd..acdef483ae 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace O3DE::ProjectManager { @@ -299,23 +300,50 @@ namespace O3DE::ProjectManager AZ_Assert(gemModel, "Failed to obtain GemModel"); QVector dependencies = gemModel->GatherGemDependencies(modelIndex); + uint32_t numChangedDependencies = 0; + if (IsAdded(modelIndex)) { for (const QModelIndex& dependency : dependencies) { - SetIsAddedDependency(*gemModel, dependency, true); + if (!IsAddedDependency(dependency)) + { + SetIsAddedDependency(*gemModel, dependency, true); + + // if the gem was already added then the state didn't really change + if (!IsAdded(dependency)) + { + numChangedDependencies++; + } + } } } else { // still a dependency if some added gem depends on this one - SetIsAddedDependency(model, modelIndex, gemModel->HasDependentGems(modelIndex)); + bool hasDependentGems = gemModel->HasDependentGems(modelIndex); + if (IsAddedDependency(modelIndex) != hasDependentGems) + { + SetIsAddedDependency(model, modelIndex, hasDependentGems); + } for (const QModelIndex& dependency : dependencies) { - SetIsAddedDependency(*gemModel, dependency, gemModel->HasDependentGems(dependency)); + hasDependentGems = gemModel->HasDependentGems(dependency); + if (IsAddedDependency(dependency) != hasDependentGems) + { + SetIsAddedDependency(*gemModel, dependency, hasDependentGems); + + // if the gem was already added then the state didn't really change + if (!IsAdded(dependency)) + { + numChangedDependencies++; + } + } } } + + gemModel->emit gemStatusChanged(modelIndex, numChangedDependencies); } void GemModel::SetIsAddedDependency(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded) @@ -488,5 +516,4 @@ namespace O3DE::ProjectManager } return result; } - } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h index 0d1c225f74..938543eb39 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h @@ -77,6 +77,9 @@ namespace O3DE::ProjectManager int TotalAddedGems(bool includeDependencies = false) const; + signals: + void gemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies); + private: void FindGemDisplayNamesByNameStrings(QStringList& inOutGemNames); void GetAllDependingGems(const QModelIndex& modelIndex, QSet& inOutGems); diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp index 633116e8b6..2bf2736c69 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp +++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp @@ -223,6 +223,7 @@ namespace RedirectOutput } } // namespace RedirectOutput + namespace O3DE::ProjectManager { PythonBindings::PythonBindings(const AZ::IO::PathView& enginePath) @@ -301,6 +302,7 @@ namespace O3DE::ProjectManager m_enableGemProject = pybind11::module::import("o3de.enable_gem"); m_disableGemProject = pybind11::module::import("o3de.disable_gem"); m_editProjectProperties = pybind11::module::import("o3de.project_properties"); + m_download = pybind11::module::import("o3de.download"); m_pathlib = pybind11::module::import("pathlib"); // make sure the engine is registered @@ -555,6 +557,47 @@ namespace O3DE::ProjectManager return AZ::Success(AZStd::move(gemNames)); } + AZ::Outcome PythonBindings::RegisterGem(const QString& gemPath, const QString& projectPath) + { + bool registrationResult = false; + auto result = ExecuteWithLockErrorHandling( + [&] + { + auto externalProjectPath = projectPath.isEmpty() ? pybind11::none() : QString_To_Py_Path(projectPath); + auto pythonRegistrationResult = m_register.attr("register")( + pybind11::none(), // engine_path + pybind11::none(), // project_path + QString_To_Py_Path(gemPath), // gem folder + pybind11::none(), // external subdirectory + pybind11::none(), // template_path + pybind11::none(), // restricted folder + pybind11::none(), // repo uri + pybind11::none(), // default_engines_folder + pybind11::none(), // default_projects_folder + pybind11::none(), // default_gems_folder + pybind11::none(), // default_templates_folder + pybind11::none(), // default_restricted_folder + pybind11::none(), // default_third_party_folder + pybind11::none(), // external_subdir_engine_path + externalProjectPath // external_subdir_project_path + ); + + // Returns an exit code so boolify it then invert result + registrationResult = !pythonRegistrationResult.cast(); + }); + + if (!result.IsSuccess()) + { + return AZ::Failure(result.GetError().c_str()); + } + else if (!registrationResult) + { + return AZ::Failure(AZStd::string::format("Failed to register gem path %s", gemPath.toUtf8().constData())); + } + + return AZ::Success(); + } + bool PythonBindings::AddProject(const QString& path) { bool registrationResult = false; @@ -1075,4 +1118,46 @@ namespace O3DE::ProjectManager std::sort(gemRepos.begin(), gemRepos.end()); return AZ::Success(AZStd::move(gemRepos)); } + + AZ::Outcome PythonBindings::DownloadGem(const QString& gemName, std::function gemProgressCallback) + { + // This process is currently limited to download a single gem at a time. + bool downloadSucceeded = false; + + m_requestCancelDownload = false; + auto result = ExecuteWithLockErrorHandling( + [&] + { + auto downloadResult = m_download.attr("download_gem")( + QString_To_Py_String(gemName), // gem name + pybind11::none(), // destination path + false, // skip auto register + pybind11::cpp_function( + [this, gemProgressCallback](int progress) + { + gemProgressCallback(progress); + + return m_requestCancelDownload; + }) // Callback for download progress and cancelling + ); + downloadSucceeded = (downloadResult.cast() == 0); + }); + + + if (!result.IsSuccess()) + { + return result; + } + else if (!downloadSucceeded) + { + return AZ::Failure("Failed to download gem."); + } + + return AZ::Success(); + } + + void PythonBindings::CancelDownload() + { + m_requestCancelDownload = true; + } } diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.h b/Code/Tools/ProjectManager/Source/PythonBindings.h index d0704d0bd1..1702e9939a 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.h +++ b/Code/Tools/ProjectManager/Source/PythonBindings.h @@ -42,6 +42,7 @@ namespace O3DE::ProjectManager AZ::Outcome, AZStd::string> GetEngineGemInfos() override; AZ::Outcome, AZStd::string> GetAllGemInfos(const QString& projectPath) override; AZ::Outcome, AZStd::string> GetEnabledGemNames(const QString& projectPath) override; + AZ::Outcome RegisterGem(const QString& gemPath, const QString& projectPath = {}) override; // Project AZ::Outcome CreateProject(const QString& projectTemplatePath, const ProjectInfo& projectInfo) override; @@ -61,6 +62,8 @@ namespace O3DE::ProjectManager bool AddGemRepo(const QString& repoUri) override; bool RemoveGemRepo(const QString& repoUri) override; AZ::Outcome, AZStd::string> GetAllGemRepoInfos() override; + AZ::Outcome DownloadGem(const QString& gemName, std::function gemProgressCallback) override; + void CancelDownload() override; private: AZ_DISABLE_COPY_MOVE(PythonBindings); @@ -87,6 +90,9 @@ namespace O3DE::ProjectManager pybind11::handle m_enableGemProject; pybind11::handle m_disableGemProject; pybind11::handle m_editProjectProperties; + pybind11::handle m_download; pybind11::handle m_pathlib; + + bool m_requestCancelDownload = false; }; } diff --git a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h index 5daf543c11..46b6c367f3 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h +++ b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h @@ -91,6 +91,14 @@ namespace O3DE::ProjectManager */ virtual AZ::Outcome, AZStd::string> GetEnabledGemNames(const QString& projectPath) = 0; + /** + * Registers the gem to the specified project, or to the o3de_manifest.json if no project path is given + * @param gemPath the path to the gem + * @param projectPath the path to the project. If empty, will register the external path in o3de_manifest.json + * @return An outcome with the success flag as well as an error message in case of a failure. + */ + virtual AZ::Outcome RegisterGem(const QString& gemPath, const QString& projectPath = {}) = 0; + // Projects @@ -187,6 +195,19 @@ namespace O3DE::ProjectManager * @return A list of gem repo infos. */ virtual AZ::Outcome, AZStd::string> GetAllGemRepoInfos() = 0; + + /** + * Downloads and registers a Gem. + * @param gemName the name of the Gem to download + * @param gemProgressCallback a callback function that is called with an int percentage download value + * @return an outcome with a string error message on failure. + */ + virtual AZ::Outcome DownloadGem(const QString& gemName, std::function gemProgressCallback) = 0; + + /** + * Cancels the current download. + */ + virtual void CancelDownload() = 0; }; using PythonBindingsInterface = AZ::Interface; diff --git a/Code/Tools/ProjectManager/Source/ScreenWidget.h b/Code/Tools/ProjectManager/Source/ScreenWidget.h index 9563fc2f6a..148dcdb8c8 100644 --- a/Code/Tools/ProjectManager/Source/ScreenWidget.h +++ b/Code/Tools/ProjectManager/Source/ScreenWidget.h @@ -47,6 +47,14 @@ namespace O3DE::ProjectManager return tr("Missing"); } + virtual bool ContainsScreen([[maybe_unused]] ProjectManagerScreen screen) + { + return false; + } + virtual void GoToScreen([[maybe_unused]] ProjectManagerScreen screen) + { + } + //! Notify this screen it is the current screen virtual void NotifyCurrentScreen() { @@ -55,7 +63,7 @@ namespace O3DE::ProjectManager signals: void ChangeScreenRequest(ProjectManagerScreen screen); - void GotoPreviousScreenRequest(); + void GoToPreviousScreenRequest(); void ResetScreenRequest(ProjectManagerScreen screen); void NotifyCurrentProject(const QString& projectPath); void NotifyBuildProject(const ProjectInfo& projectInfo); diff --git a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp index df0bdb29f4..314765def0 100644 --- a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp @@ -83,11 +83,28 @@ namespace O3DE::ProjectManager bool ScreensCtrl::ForceChangeToScreen(ProjectManagerScreen screen, bool addVisit) { + ScreenWidget* newScreen = nullptr; + const auto iterator = m_screenMap.find(screen); if (iterator != m_screenMap.end()) + { + newScreen = iterator.value(); + } + else + { + // Check if screen is contained by another screen + for (ScreenWidget* checkingScreen : m_screenMap) + { + if (checkingScreen->ContainsScreen(screen)) + { + newScreen = checkingScreen; + break; + } + } + } + if (newScreen) { ScreenWidget* currentScreen = GetCurrentScreen(); - ScreenWidget* newScreen = iterator.value(); if (currentScreen != newScreen) { @@ -109,6 +126,11 @@ namespace O3DE::ProjectManager newScreen->NotifyCurrentScreen(); + if (iterator == m_screenMap.end()) + { + newScreen->GoToScreen(screen); + } + return true; } } @@ -116,7 +138,7 @@ namespace O3DE::ProjectManager return false; } - bool ScreensCtrl::GotoPreviousScreen() + bool ScreensCtrl::GoToPreviousScreen() { if (!m_screenVisitOrder.isEmpty()) { @@ -171,7 +193,7 @@ namespace O3DE::ProjectManager m_screenMap.insert(screen, newScreen); connect(newScreen, &ScreenWidget::ChangeScreenRequest, this, &ScreensCtrl::ChangeToScreen); - connect(newScreen, &ScreenWidget::GotoPreviousScreenRequest, this, &ScreensCtrl::GotoPreviousScreen); + connect(newScreen, &ScreenWidget::GoToPreviousScreenRequest, this, &ScreensCtrl::GoToPreviousScreen); connect(newScreen, &ScreenWidget::ResetScreenRequest, this, &ScreensCtrl::ResetScreen); connect(newScreen, &ScreenWidget::NotifyCurrentProject, this, &ScreensCtrl::NotifyCurrentProject); connect(newScreen, &ScreenWidget::NotifyBuildProject, this, &ScreensCtrl::NotifyBuildProject); diff --git a/Code/Tools/ProjectManager/Source/ScreensCtrl.h b/Code/Tools/ProjectManager/Source/ScreensCtrl.h index 7132d64dd0..ab69a09ea3 100644 --- a/Code/Tools/ProjectManager/Source/ScreensCtrl.h +++ b/Code/Tools/ProjectManager/Source/ScreensCtrl.h @@ -41,7 +41,7 @@ namespace O3DE::ProjectManager public slots: bool ChangeToScreen(ProjectManagerScreen screen); bool ForceChangeToScreen(ProjectManagerScreen screen, bool addVisit = true); - bool GotoPreviousScreen(); + bool GoToPreviousScreen(); void ResetScreen(ProjectManagerScreen screen); void ResetAllScreens(); void DeleteScreen(ProjectManagerScreen screen); diff --git a/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp b/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp index e952ada57a..1c8f9a6931 100644 --- a/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp @@ -40,6 +40,10 @@ namespace O3DE::ProjectManager m_updateSettingsScreen = new UpdateProjectSettingsScreen(); m_gemCatalogScreen = new GemCatalogScreen(); + connect(m_gemCatalogScreen, &ScreenWidget::ChangeScreenRequest, this, [this](ProjectManagerScreen screen){ + emit ChangeScreenRequest(screen); + }); + m_stack = new QStackedWidget(this); m_stack->setObjectName("body"); m_stack->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding)); @@ -118,7 +122,7 @@ namespace O3DE::ProjectManager { if (UpdateProjectSettings(true)) { - emit GotoPreviousScreenRequest(); + emit GoToPreviousScreenRequest(); } } } @@ -136,6 +140,11 @@ namespace O3DE::ProjectManager } else if (m_stack->currentIndex() == ScreenOrder::Gems && m_gemCatalogScreen) { + if (!m_gemCatalogScreen->GetDownloadController()->IsDownloadQueueEmpty()) + { + QMessageBox::critical(this, tr("Gems downloading"), tr("You must wait for gems to finish downloading before continuing.")); + return; + } // Enable or disable the gems that got adjusted in the gem catalog and apply them to the given project. const GemCatalogScreen::EnableDisableGemsResult result = m_gemCatalogScreen->EnableDisableGemsForProject(m_projectInfo.m_path); if (result == GemCatalogScreen::EnableDisableGemsResult::Failed) diff --git a/Code/Tools/ProjectManager/project_manager_files.cmake b/Code/Tools/ProjectManager/project_manager_files.cmake index fd8389ca4f..e2e35717f6 100644 --- a/Code/Tools/ProjectManager/project_manager_files.cmake +++ b/Code/Tools/ProjectManager/project_manager_files.cmake @@ -29,6 +29,10 @@ set(FILES Source/FormImageBrowseEditWidget.cpp Source/GemsSubWidget.h Source/GemsSubWidget.cpp + Source/DownloadController.h + Source/DownloadController.cpp + Source/DownloadWorker.h + Source/DownloadWorker.cpp Source/PathValidator.h Source/PathValidator.cpp Source/ProjectManagerWindow.h diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/IAWSGameLiftRequests.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftMatchmakingRequestBus.h similarity index 53% rename from Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/IAWSGameLiftRequests.h rename to Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftMatchmakingRequestBus.h index c14ef559b2..8ab215d741 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/IAWSGameLiftRequests.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftMatchmakingRequestBus.h @@ -5,75 +5,16 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT * */ - + #pragma once #include -#include #include #include -#include namespace AWSGameLift { - //! IAWSGameLiftRequests - //! GameLift Gem interfaces to configure client manager - class IAWSGameLiftRequests - { - public: - AZ_RTTI(IAWSGameLiftRequests, "{494167AD-1185-4AF3-8BF9-C8C37FC9C199}"); - - IAWSGameLiftRequests() = default; - virtual ~IAWSGameLiftRequests() = default; - - //! ConfigureGameLiftClient - //! Configure GameLift client to interact with Amazon GameLift service - //! @param region Specifies the AWS region to use - //! @return True if client configuration succeeds, false otherwise - virtual bool ConfigureGameLiftClient(const AZStd::string& region) = 0; - - //! CreatePlayerId - //! Create a new, random ID number for every player in every new game session. - //! @param includeBrackets Whether includes brackets in player id - //! @param includeDashes Whether includes dashes in player id - //! @return The player id to use in game session - virtual AZStd::string CreatePlayerId(bool includeBrackets, bool includeDashes) = 0; - }; - - // IAWSGameLiftRequests EBus wrapper for scripting - class AWSGameLiftRequests - : public AZ::EBusTraits - { - public: - using MutexType = AZStd::recursive_mutex; - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - }; - using AWSGameLiftRequestBus = AZ::EBus; - - // ISessionAsyncRequests EBus wrapper for scripting - class AWSGameLiftSessionAsyncRequests - : public AZ::EBusTraits - { - public: - using MutexType = AZStd::recursive_mutex; - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - }; - using AWSGameLiftSessionAsyncRequestBus = AZ::EBus; - - // ISessionRequests EBus wrapper for scripting - class AWSGameLiftSessionRequests - : public AZ::EBusTraits - { - public: - using MutexType = AZStd::recursive_mutex; - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - }; - using AWSGameLiftSessionRequestBus = AZ::EBus; - - // IMatchmakingAsyncRequests EBus wrapper for scripting + // IMatchmakingAsyncRequests EBus wrapper class AWSGameLiftMatchmakingAsyncRequests : public AZ::EBusTraits { @@ -84,7 +25,7 @@ namespace AWSGameLift }; using AWSGameLiftMatchmakingAsyncRequestBus = AZ::EBus; - // IMatchmakingRequests EBus wrapper for scripting + // IMatchmakingRequests EBus wrapper class AWSGameLiftMatchmakingRequests : public AZ::EBusTraits { @@ -121,7 +62,7 @@ namespace AWSGameLift virtual void StopPolling() = 0; }; - // IAWSGameLiftMatchmakingEventRequests EBus wrapper for scripting + // IAWSGameLiftMatchmakingEventRequests EBus wrapper class AWSGameLiftMatchmakingEventRequests : public AZ::EBusTraits { diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftRequestBus.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftRequestBus.h new file mode 100644 index 0000000000..e1951d1e61 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftRequestBus.h @@ -0,0 +1,51 @@ +/* + * 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 + +namespace AWSGameLift +{ + //! IAWSGameLiftRequests + //! GameLift Gem interfaces to configure GameLift client and other help functions, + //! like creating random GameLift player id + class IAWSGameLiftRequests + { + public: + AZ_RTTI(IAWSGameLiftRequests, "{494167AD-1185-4AF3-8BF9-C8C37FC9C199}"); + + IAWSGameLiftRequests() = default; + virtual ~IAWSGameLiftRequests() = default; + + //! ConfigureGameLiftClient + //! Configure GameLift client to interact with Amazon GameLift service + //! @param region Specifies the AWS region to use + //! @return True if client configuration succeeds, false otherwise + virtual bool ConfigureGameLiftClient(const AZStd::string& region) = 0; + + //! CreatePlayerId + //! Create a new, random ID number for every player in every new game session. + //! @param includeBrackets Whether includes brackets in player id + //! @param includeDashes Whether includes dashes in player id + //! @return The player id to use in game session + virtual AZStd::string CreatePlayerId(bool includeBrackets, bool includeDashes) = 0; + }; + + // IAWSGameLiftRequests EBus wrapper + class AWSGameLiftRequests + : public AZ::EBusTraits + { + public: + using MutexType = AZStd::recursive_mutex; + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + }; + using AWSGameLiftRequestBus = AZ::EBus; +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftSessionRequestBus.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftSessionRequestBus.h new file mode 100644 index 0000000000..c99509ca3f --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftSessionRequestBus.h @@ -0,0 +1,38 @@ +/* + * 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 AWSGameLift +{ + // ISessionAsyncRequests EBus wrapper + class AWSGameLiftSessionAsyncRequests + : public AZ::EBusTraits + { + public: + using MutexType = AZStd::recursive_mutex; + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + }; + using AWSGameLiftSessionAsyncRequestBus = AZ::EBus; + + // ISessionRequests EBus wrapper + class AWSGameLiftSessionRequests + : public AZ::EBusTraits + { + public: + using MutexType = AZStd::recursive_mutex; + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + }; + using AWSGameLiftSessionRequestBus = AZ::EBus; +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.cpp index cc2f84cb35..2e978402cd 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.cpp @@ -97,6 +97,7 @@ namespace AWSGameLift AZ_TracePrintf(AWSGameLiftClientLocalTicketTrackerName, "Matchmaking ticket %s is complete.", ticket.GetTicketId().c_str()); RequestPlayerJoinMatch(ticket, playerId); + AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchComplete); m_status = TicketTrackerStatus::Idle; return; } @@ -104,25 +105,28 @@ namespace AWSGameLift ticket.GetStatus() == Aws::GameLift::Model::MatchmakingConfigurationStatus::FAILED || ticket.GetStatus() == Aws::GameLift::Model::MatchmakingConfigurationStatus::CANCELLED) { - AZ_Error(AWSGameLiftClientLocalTicketTrackerName, false, "Matchmaking ticket %s is not complete, %s", - ticket.GetTicketId().c_str(), ticket.GetStatusReason().c_str()); + AZ_Warning(AWSGameLiftClientLocalTicketTrackerName, false, "Matchmaking ticket %s is not complete, %s", + ticket.GetTicketId().c_str(), ticket.GetStatusMessage().c_str()); + AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchFailure); m_status = TicketTrackerStatus::Idle; return; } else if (ticket.GetStatus() == Aws::GameLift::Model::MatchmakingConfigurationStatus::REQUIRES_ACCEPTANCE) { - // broadcast acceptance requires to player - AzFramework::MatchAcceptanceNotificationBus::Broadcast(&AzFramework::MatchAcceptanceNotifications::OnMatchAcceptance); + AZ_TracePrintf(AWSGameLiftClientLocalTicketTrackerName, "Matchmaking ticket %s is pending on acceptance, %s.", + ticket.GetTicketId().c_str(), ticket.GetStatusMessage().c_str()); + AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchAcceptance); } else { AZ_TracePrintf(AWSGameLiftClientLocalTicketTrackerName, "Matchmaking ticket %s is processing, %s.", - ticket.GetTicketId().c_str(), ticket.GetStatusReason().c_str()); + ticket.GetTicketId().c_str(), ticket.GetStatusMessage().c_str()); } } else { AZ_Error(AWSGameLiftClientLocalTicketTrackerName, false, "Unable to find expected ticket with id %s", ticketId.c_str()); + AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchError); } } else @@ -130,11 +134,13 @@ namespace AWSGameLift AZ_Error(AWSGameLiftClientLocalTicketTrackerName, false, AWSGameLiftErrorMessageTemplate, describeMatchmakingOutcome.GetError().GetExceptionName().c_str(), describeMatchmakingOutcome.GetError().GetMessage().c_str()); + AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchError); } } else { AZ_Error(AWSGameLiftClientLocalTicketTrackerName, false, AWSGameLiftClientMissingErrorMessage); + AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchError); } m_waitEvent.try_acquire_for(AZStd::chrono::milliseconds(m_pollingPeriodInMS)); } diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.h index 04bdd71c85..9fd7f76e1d 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.h @@ -12,7 +12,7 @@ #include #include -#include +#include #include diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.cpp index 224f16481e..4ee2d31ebf 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -75,7 +76,15 @@ namespace AWSGameLift bool AWSGameLiftClientManager::ConfigureGameLiftClient(const AZStd::string& region) { AZ::Interface::Get()->SetGameLiftClient(nullptr); + Aws::Client::ClientConfiguration clientConfig; + AWSCore::AwsApiJobConfig* defaultConfig = nullptr; + AWSCore::AWSCoreRequestBus::BroadcastResult(defaultConfig, &AWSCore::AWSCoreRequests::GetDefaultConfig); + if (defaultConfig) + { + clientConfig = defaultConfig->GetClientConfiguration(); + } + // Set up client endpoint or region AZStd::string localEndpoint = ""; #if defined(AWSGAMELIFT_DEV) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.h index 1f32b69f75..8a0c91c36d 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.h @@ -8,10 +8,13 @@ #pragma once +#include #include #include -#include +#include +#include +#include namespace AWSGameLift { @@ -23,22 +26,37 @@ namespace AWSGameLift struct AWSGameLiftStartMatchmakingRequest; struct AWSGameLiftStopMatchmakingRequest; - // MatchAcceptanceNotificationBus EBus handler for scripting - class AWSGameLiftMatchAcceptanceNotificationBusHandler - : public AzFramework::MatchAcceptanceNotificationBus::Handler + // MatchmakingNotificationBus EBus handler for scripting + class AWSGameLiftMatchmakingNotificationBusHandler + : public AzFramework::MatchmakingNotificationBus::Handler , public AZ::BehaviorEBusHandler { public: AZ_EBUS_BEHAVIOR_BINDER( - AWSGameLiftMatchAcceptanceNotificationBusHandler, + AWSGameLiftMatchmakingNotificationBusHandler, "{CBE057D3-F5CE-46D3-B02D-8A6A1446B169}", AZ::SystemAllocator, - OnMatchAcceptance); + OnMatchAcceptance, OnMatchComplete, OnMatchError, OnMatchFailure); void OnMatchAcceptance() override { Call(FN_OnMatchAcceptance); } + + void OnMatchComplete() override + { + Call(FN_OnMatchComplete); + } + + void OnMatchError() override + { + Call(FN_OnMatchError); + } + + void OnMatchFailure() override + { + Call(FN_OnMatchFailure); + } }; // MatchmakingAsyncRequestNotificationBus EBus handler for scripting diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp index 981c1599c9..d256c6c33c 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp @@ -65,13 +65,6 @@ namespace AWSGameLift ->Event("CreatePlayerId", &AWSGameLiftRequestBus::Events::CreatePlayerId, { { { "IncludeBrackets", "" }, { "IncludeDashes", "" } } }); - - behaviorContext->EBus("AWSGameLiftMatchmakingEventRequestBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") - ->Event("StartPolling", &AWSGameLiftMatchmakingEventRequestBus::Events::StartPolling, - { { { "TicketId", "" }, - { "PlayerId", "" } } }) - ->Event("StopPolling", &AWSGameLiftMatchmakingEventRequestBus::Events::StopPolling); } } @@ -128,7 +121,7 @@ namespace AWSGameLift if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) { behaviorContext->EBus("AWSGameLiftMatchmakingAsyncRequestBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking") ->Event("AcceptMatchAsync", &AWSGameLiftMatchmakingAsyncRequestBus::Events::AcceptMatchAsync, { { { "AcceptMatchRequest", "" } } }) ->Event("StartMatchmakingAsync", &AWSGameLiftMatchmakingAsyncRequestBus::Events::StartMatchmakingAsync, @@ -137,20 +130,28 @@ namespace AWSGameLift { { { "StopMatchmakingRequest", "" } } }); behaviorContext->EBus("AWSGameLiftMatchmakingAsyncRequestNotificationBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking") ->Handler(); behaviorContext->EBus("AWSGameLiftMatchmakingRequestBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") - ->Event("AcceptMatch", &AWSGameLiftMatchmakingRequestBus::Events::AcceptMatch, { { { "AcceptMatchRequest", "" } } }) + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking") + ->Event("AcceptMatch", &AWSGameLiftMatchmakingRequestBus::Events::AcceptMatch, + { { { "AcceptMatchRequest", "" } } }) ->Event("StartMatchmaking", &AWSGameLiftMatchmakingRequestBus::Events::StartMatchmaking, { { { "StartMatchmakingRequest", "" } } }) ->Event("StopMatchmaking", &AWSGameLiftMatchmakingRequestBus::Events::StopMatchmaking, { { { "StopMatchmakingRequest", "" } } }); - behaviorContext->EBus("AWSGameLiftMatchAcceptanceNotificationBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") - ->Handler(); + behaviorContext->EBus("AWSGameLiftMatchmakingEventRequestBus") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking") + ->Event("StartPolling", &AWSGameLiftMatchmakingEventRequestBus::Events::StartPolling, + { { { "TicketId", "" }, + { "PlayerId", "" } } }) + ->Event("StopPolling", &AWSGameLiftMatchmakingEventRequestBus::Events::StopPolling); + + behaviorContext->EBus("AWSGameLiftMatchmakingNotificationBus") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking") + ->Handler(); } } @@ -166,7 +167,7 @@ namespace AWSGameLift if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) { behaviorContext->EBus("AWSGameLiftSessionAsyncRequestBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Session") ->Event("CreateSessionAsync", &AWSGameLiftSessionAsyncRequestBus::Events::CreateSessionAsync, { { { "CreateSessionRequest", "" } } }) ->Event("JoinSessionAsync", &AWSGameLiftSessionAsyncRequestBus::Events::JoinSessionAsync, { { { "JoinSessionRequest", "" } } }) @@ -175,11 +176,11 @@ namespace AWSGameLift ->Event("LeaveSessionAsync", &AWSGameLiftSessionAsyncRequestBus::Events::LeaveSessionAsync); behaviorContext->EBus("AWSGameLiftSessionAsyncRequestNotificationBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Session") ->Handler(); behaviorContext->EBus("AWSGameLiftSessionRequestBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Session") ->Event("CreateSession", &AWSGameLiftSessionRequestBus::Events::CreateSession, { { { "CreateSessionRequest", "" } } }) ->Event("JoinSession", &AWSGameLiftSessionRequestBus::Events::JoinSession, { { { "JoinSessionRequest", "" } } }) ->Event("SearchSessions", &AWSGameLiftSessionRequestBus::Events::SearchSessions, { { { "SearchSessionsRequest", "" } } }) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/IAWSGameLiftInternalRequests.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/IAWSGameLiftInternalRequests.h index dbac9a798a..7e17f085b3 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/IAWSGameLiftInternalRequests.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/IAWSGameLiftInternalRequests.h @@ -20,7 +20,7 @@ namespace Aws namespace AWSGameLift { - //! IAWSGameLiftRequests + //! IAWSGameLiftInternalRequests //! GameLift Gem internal interface which is used to fetch gem global GameLift client class IAWSGameLiftInternalRequests { diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientLocalTicketTrackerTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientLocalTicketTrackerTest.cpp index 4dc4dd85f6..be72de555e 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientLocalTicketTrackerTest.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientLocalTicketTrackerTest.cpp @@ -82,27 +82,12 @@ protected: m_gameliftClientMockPtr.reset(); } - void WaitForProcessFinish(uint64_t expectedNum) + void WaitForProcessFinish(AZStd::function processFinishCondition) { int processingTime = 0; while (processingTime < TEST_WAIT_MAXIMUM_TIME_MS) { - if (::UnitTest::TestRunner::Instance().m_numAssertsFailed == expectedNum) - { - AZ_TEST_STOP_TRACE_SUPPRESSION(expectedNum); - return; - } - AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(TEST_WAIT_BUFFER_TIME_MS)); - processingTime += TEST_WAIT_BUFFER_TIME_MS; - } - } - - void WaitForProcessFinish() - { - int processingTime = 0; - while (processingTime < TEST_WAIT_MAXIMUM_TIME_MS) - { - if (m_gameliftClientTicketTracker->IsTrackerIdle()) + if (processFinishCondition()) { return; } @@ -119,19 +104,27 @@ public: TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallWithoutClientSetup_GetExpectedErrors) { AZ::Interface::Get()->SetGameLiftClient(nullptr); + + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1); ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle()); } TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_MultipleCallsWithoutClientSetup_GetExpectedErrors) { AZ::Interface::Get()->SetGameLiftClient(nullptr); + + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1); ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -144,9 +137,12 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButWithFailedOu .Times(1) .WillOnce(::testing::Return(outcome)); + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1); ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -161,9 +157,12 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallWithMoreThanOne .Times(1) .WillOnce(::testing::Return(outcome)); + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1); ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -189,13 +188,15 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallWithCompleteSta .Times(1) .WillOnce(::testing::Return(outcome)); - SessionHandlingClientRequestsMock handlerMock; - EXPECT_CALL(handlerMock, RequestPlayerJoinSession(::testing::_)) + SessionHandlingClientRequestsMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, RequestPlayerJoinSession(::testing::_)) .Times(1) .WillOnce(::testing::Return(true)); + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -217,9 +218,12 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButNoPlayerSess .Times(1) .WillOnce(::testing::Return(outcome)); + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -245,14 +249,17 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButFailedToJoin .Times(1) .WillOnce(::testing::Return(outcome)); - SessionHandlingClientRequestsMock handlerMock; - EXPECT_CALL(handlerMock, RequestPlayerJoinSession(::testing::_)) + SessionHandlingClientRequestsMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, RequestPlayerJoinSession(::testing::_)) .Times(1) .WillOnce(::testing::Return(false)); + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -269,9 +276,10 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButTicketTimeOu .Times(1) .WillOnce(::testing::Return(outcome)); - AZ_TEST_START_TRACE_SUPPRESSION; + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchFailure == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -288,9 +296,10 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButTicketFailed .Times(1) .WillOnce(::testing::Return(outcome)); - AZ_TEST_START_TRACE_SUPPRESSION; + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchFailure == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -307,9 +316,10 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButTicketCancel .Times(1) .WillOnce(::testing::Return(outcome)); - AZ_TEST_START_TRACE_SUPPRESSION; + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchFailure == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -342,13 +352,15 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallAndTicketComple .WillOnce(::testing::Return(outcome1)) .WillOnce(::testing::Return(outcome2)); - SessionHandlingClientRequestsMock handlerMock; - EXPECT_CALL(handlerMock, RequestPlayerJoinSession(::testing::_)) + SessionHandlingClientRequestsMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, RequestPlayerJoinSession(::testing::_)) .Times(1) .WillOnce(::testing::Return(true)); + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -365,7 +377,9 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_RequiresAcceptanceA connectionInfo.SetIpAddress("DummyIpAddress"); connectionInfo.SetPort(123); connectionInfo.AddMatchedPlayerSessions( - Aws::GameLift::Model::MatchedPlayerSession().WithPlayerId("player1").WithPlayerSessionId("playersession1")); + Aws::GameLift::Model::MatchedPlayerSession() + .WithPlayerId("player1") + .WithPlayerSessionId("playersession1")); Aws::GameLift::Model::MatchmakingTicket ticket2; ticket2.SetStatus(Aws::GameLift::Model::MatchmakingConfigurationStatus::COMPLETED); @@ -379,13 +393,15 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_RequiresAcceptanceA .WillOnce(::testing::Return(outcome1)) .WillOnce(::testing::Return(outcome2)); - MatchAcceptanceNotificationsHandlerMock handlerMock1; - EXPECT_CALL(handlerMock1, OnMatchAcceptance()).Times(1); - - SessionHandlingClientRequestsMock handlerMock2; - EXPECT_CALL(handlerMock2, RequestPlayerJoinSession(::testing::_)).Times(1).WillOnce(::testing::Return(true)); + SessionHandlingClientRequestsMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, RequestPlayerJoinSession(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(true)); + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchAcceptance == 1); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp index 1c3e8726fd..6ce0cb8f64 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp @@ -266,6 +266,8 @@ const char* const AWSGameLiftClientManagerTest::DummyPlayerId = "dummyPlayerId"; TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithoutRegion_GetFalseAsResult) { + AWSCoreRequestsHandlerMock coreHandlerMock; + EXPECT_CALL(coreHandlerMock, GetDefaultConfig()).Times(1).WillOnce(nullptr); AZ_TEST_START_TRACE_SUPPRESSION; auto result = m_gameliftClientManager->ConfigureGameLiftClient(""); AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message @@ -274,6 +276,8 @@ TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithoutRegion_G TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithoutCredential_GetFalseAsResult) { + AWSCoreRequestsHandlerMock coreHandlerMock; + EXPECT_CALL(coreHandlerMock, GetDefaultConfig()).Times(1).WillOnce(nullptr); AWSResourceMappingRequestsHandlerMock handlerMock; EXPECT_CALL(handlerMock, GetDefaultRegion()).Times(1).WillOnce(::testing::Return("us-west-2")); AZ_TEST_START_TRACE_SUPPRESSION; @@ -284,6 +288,8 @@ TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithoutCredenti TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithRegionAndCredential_GetTrueAsResult) { + AWSCoreRequestsHandlerMock coreHandlerMock; + EXPECT_CALL(coreHandlerMock, GetDefaultConfig()).Times(1).WillOnce(nullptr); AWSCredentialRequestsHandlerMock handlerMock; EXPECT_CALL(handlerMock, GetCredentialsProvider()) .Times(1) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientMocks.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientMocks.h index 01afec5c3f..d685f61d30 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientMocks.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientMocks.h @@ -9,9 +9,9 @@ #pragma once #include -#include #include #include +#include #include #include @@ -76,21 +76,44 @@ public: MOCK_METHOD0(OnStopMatchmakingAsyncComplete, void()); }; -class MatchAcceptanceNotificationsHandlerMock - : public AzFramework::MatchAcceptanceNotificationBus::Handler +class MatchmakingNotificationsHandlerMock + : public AzFramework::MatchmakingNotificationBus::Handler { public: - MatchAcceptanceNotificationsHandlerMock() + MatchmakingNotificationsHandlerMock() + { + AzFramework::MatchmakingNotificationBus::Handler::BusConnect(); + } + + ~MatchmakingNotificationsHandlerMock() + { + AzFramework::MatchmakingNotificationBus::Handler::BusDisconnect(); + } + + void OnMatchAcceptance() override + { + ++m_numMatchAcceptance; + } + + void OnMatchComplete() override + { + ++m_numMatchComplete; + } + + void OnMatchError() override { - AzFramework::MatchAcceptanceNotificationBus::Handler::BusConnect(); + ++m_numMatchError; } - ~MatchAcceptanceNotificationsHandlerMock() + void OnMatchFailure() override { - AzFramework::MatchAcceptanceNotificationBus::Handler::BusDisconnect(); + ++m_numMatchFailure; } - MOCK_METHOD0(OnMatchAcceptance, void()); + int m_numMatchAcceptance = 0; + int m_numMatchComplete = 0; + int m_numMatchError = 0; + int m_numMatchFailure = 0; }; class SessionAsyncRequestNotificationsHandlerMock diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake b/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake index 629d1596cf..fe22e0c65c 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake @@ -17,7 +17,9 @@ set(FILES Include/Request/AWSGameLiftSearchSessionsRequest.h Include/Request/AWSGameLiftStartMatchmakingRequest.h Include/Request/AWSGameLiftStopMatchmakingRequest.h - Include/Request/IAWSGameLiftRequests.h + Include/Request/AWSGameLiftRequestBus.h + Include/Request/AWSGameLiftSessionRequestBus.h + Include/Request/AWSGameLiftMatchmakingRequestBus.h Source/Activity/AWSGameLiftActivityUtils.cpp Source/Activity/AWSGameLiftActivityUtils.h Source/Activity/AWSGameLiftAcceptMatchActivity.cpp diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/IAWSGameLiftServerRequests.h b/Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/AWSGameLiftServerRequestBus.h similarity index 97% rename from Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/IAWSGameLiftServerRequests.h rename to Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/AWSGameLiftServerRequestBus.h index 777086e633..27096b9fe8 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/IAWSGameLiftServerRequests.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/AWSGameLiftServerRequestBus.h @@ -5,7 +5,7 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT * */ - + #pragma once #include @@ -45,7 +45,7 @@ namespace AWSGameLift virtual bool StopMatchBackfill(const AZStd::string& ticketId) = 0; }; - // IAWSGameLiftServerRequests EBus wrapper for scripting + // IAWSGameLiftServerRequests EBus wrapper class AWSGameLiftServerRequests : public AZ::EBusTraits { diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h index ee21751fe9..6e7ce4e005 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h @@ -20,7 +20,7 @@ #include #include -#include +#include namespace AWSGameLift { diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake b/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake index 9039c9943e..70dfd38fc3 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake @@ -10,7 +10,7 @@ set(FILES ../AWSGameLiftCommon/Include/AWSGameLiftPlayer.h ../AWSGameLiftCommon/Source/AWSGameLiftPlayer.cpp ../AWSGameLiftCommon/Source/AWSGameLiftSessionConstants.h - Include/Request/IAWSGameLiftServerRequests.h + Include/Request/AWSGameLiftServerRequestBus.h Source/AWSGameLiftServerManager.cpp Source/AWSGameLiftServerManager.h Source/AWSGameLiftServerSystemComponent.cpp diff --git a/Gems/AWSMetrics/Code/Include/Private/MetricsManager.h b/Gems/AWSMetrics/Code/Include/Private/MetricsManager.h index 2cd5ba97a8..3bb06acd75 100644 --- a/Gems/AWSMetrics/Code/Include/Private/MetricsManager.h +++ b/Gems/AWSMetrics/Code/Include/Private/MetricsManager.h @@ -31,7 +31,7 @@ namespace AWSMetrics static const unsigned int DesiredMaxWorkers = 2; MetricsManager(); - ~MetricsManager(); + virtual ~MetricsManager(); //! Initializing the metrics manager //! @return Whether the operation is successful. @@ -93,6 +93,12 @@ namespace AWSMetrics //! @return Total number of requests for sending metrics events. int GetNumTotalRequests() const; + protected: + //! Send metrics to a local file. + //! @param metricsQueue metricsQueue Metrics queue that stores the metrics. + //! @return Outcome of the operation. + virtual AZ::Outcome SendMetricsToFile(AZStd::shared_ptr metricsQueue); + private: //! Job management void SetupJobContext(); @@ -112,11 +118,6 @@ namespace AWSMetrics //! @param metricsQueue Metrics events to send. void SendMetricsToServiceApiAsync(const MetricsQueue& metricsQueue); - //! Send metrics to a local file. - //! @param metricsQueue metricsQueue Metrics queue that stores the metrics. - //! @return Outcome of the operation. - AZ::Outcome SendMetricsToFile(AZStd::shared_ptr metricsQueue); - //! Push metrics events to the front of the queue for retry. //! @param metricsEventsForRetry Metrics events for retry. void PushMetricsForRetry(MetricsQueue& metricsEventsForRetry); diff --git a/Gems/AWSMetrics/Code/Tests/AWSMetricsGemMock.h b/Gems/AWSMetrics/Code/Tests/AWSMetricsGemMock.h index b77f39c93c..4f40fd6ff1 100644 --- a/Gems/AWSMetrics/Code/Tests/AWSMetricsGemMock.h +++ b/Gems/AWSMetrics/Code/Tests/AWSMetricsGemMock.h @@ -139,11 +139,6 @@ namespace AWSMetrics return true; } - bool RemoveDirectory(const AZStd::string& directory) - { - return AZ::IO::SystemFile::DeleteDir(directory.c_str()); - } - AZ::IO::FileIOBase* m_priorFileIO = nullptr; AZ::IO::FileIOBase* m_localFileIO = nullptr; diff --git a/Gems/AWSMetrics/Code/Tests/MetricsManagerTest.cpp b/Gems/AWSMetrics/Code/Tests/MetricsManagerTest.cpp index 3f87a1079d..9fcebec524 100644 --- a/Gems/AWSMetrics/Code/Tests/MetricsManagerTest.cpp +++ b/Gems/AWSMetrics/Code/Tests/MetricsManagerTest.cpp @@ -75,6 +75,23 @@ namespace AZ namespace AWSMetrics { + class MetricsManagerMock + : public MetricsManager + { + private: + AZ::Outcome SendMetricsToFile(AZStd::shared_ptr metricsQueue) override + { + if (AZ::IO::FileIOBase::GetInstance()) + { + return AZ::Success(); + } + else + { + return AZ::Failure(AZStd::string{ "Invalid File IO" }); + } + } + }; + class AWSMetricsNotificationBusMock : protected AWSMetricsNotificationBus::Handler { @@ -134,13 +151,11 @@ namespace AWSMetrics AWSMetricsGemAllocatorFixture::SetUp(); AWSMetricsRequestBus::Handler::BusConnect(); - m_metricsManager = AZStd::make_unique(); + m_metricsManager = AZStd::make_unique(); AZStd::string configFilePath = CreateClientConfigFile(true, (double) TestMetricsEventSizeInBytes / MbToBytes * 2, DefaultFlushPeriodInSeconds, 0); m_settingsRegistry->MergeSettingsFile(configFilePath, AZ::SettingsRegistryInterface::Format::JsonMergePatch, {}); m_metricsManager->Init(); - RemoveFile(m_metricsManager->GetMetricsFilePath()); - ReplaceLocalFileIOWithMockIO(); } @@ -149,8 +164,6 @@ namespace AWSMetrics RevertMockIOToLocalFileIO(); RemoveFile(GetDefaultTestFilePath()); - RemoveFile(m_metricsManager->GetMetricsFilePath()); - RemoveDirectory(m_metricsManager->GetMetricsFileDirectory()); m_metricsManager.reset(); @@ -233,7 +246,7 @@ namespace AWSMetrics } } - AZStd::unique_ptr m_metricsManager; + AZStd::unique_ptr m_metricsManager; AWSMetricsNotificationBusMock m_notifications; AZ::IO::FileIOBase* m_fileIOMock; diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslCompiler.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslCompiler.cpp index 79a39ff793..3edc7bbe67 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslCompiler.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslCompiler.cpp @@ -404,18 +404,18 @@ namespace AZ bool AzslCompiler::ParseSrgPopulateRootConstantData(const rapidjson::Document& input, RootConstantData& rootConstantData) const { - if (input.HasMember("InlineConstantBuffer")) + if (input.HasMember("RootConstantBuffer")) { - const rapidjson::Value& rootConstantBufferValue = input["InlineConstantBuffer"]; - AZ_Assert(rootConstantBufferValue.IsObject(), "InlineConstantBuffer is not an object"); + const rapidjson::Value& rootConstantBufferValue = input["RootConstantBuffer"]; + AZ_Assert(rootConstantBufferValue.IsObject(), "RootConstantBuffer is not an object"); for (rapidjson::Value::ConstMemberIterator itr = rootConstantBufferValue.MemberBegin(); itr != rootConstantBufferValue.MemberEnd(); ++itr) { AZStd::string_view rootConstantBufferMemberName = itr->name.GetString(); const rapidjson::Value& rootConstantBufferMemberValue = itr->value; - if (rootConstantBufferMemberName == "bufferForInlineConstants") + if (rootConstantBufferMemberName == "bufferForRootConstants") { - AZ_Assert(rootConstantBufferMemberValue.IsObject(), "bufferForInlineConstants is not an object"); + AZ_Assert(rootConstantBufferMemberValue.IsObject(), "bufferForRootConstants is not an object"); for (rapidjson::Value::ConstMemberIterator itr2 = rootConstantBufferMemberValue.MemberBegin(); itr2 != rootConstantBufferMemberValue.MemberEnd(); ++itr2) { @@ -442,14 +442,14 @@ namespace AZ } } } - else if (rootConstantBufferMemberName == "inputsForInlineConstants") + else if (rootConstantBufferMemberName == "inputsForRootConstants") { - AZ_Assert(rootConstantBufferMemberValue.IsArray(), "inputsForInlineConstants is not an array"); + AZ_Assert(rootConstantBufferMemberValue.IsArray(), "inputsForRootConstants is not an array"); for (rapidjson::Value::ConstValueIterator itr2 = rootConstantBufferMemberValue.Begin(); itr2 != rootConstantBufferMemberValue.End(); ++itr2) { const rapidjson::Value& rootConstantBufferValue2 = *itr2; - AZ_Assert(rootConstantBufferValue2.IsObject(), "Entry in inputsForInlineConstants is not an object"); + AZ_Assert(rootConstantBufferValue2.IsObject(), "Entry in inputsForRootConstants is not an object"); SrgConstantData rootConstantInputs; diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp index cacb310918..56f2fdec62 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp @@ -81,7 +81,7 @@ namespace AZ // Register Shader Asset Builder AssetBuilderSDK::AssetBuilderDesc shaderAssetBuilderDescriptor; shaderAssetBuilderDescriptor.m_name = "Shader Asset Builder"; - shaderAssetBuilderDescriptor.m_version = 104; // ATOM-15871 + shaderAssetBuilderDescriptor.m_version = 107; // Required .azsl extension in .shader file references // .shader file changes trigger rebuilds shaderAssetBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern( AZStd::string::format("*.%s", RPI::ShaderSourceData::Extension), AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard)); shaderAssetBuilderDescriptor.m_busId = azrtti_typeid(); @@ -96,7 +96,7 @@ namespace AZ shaderVariantAssetBuilderDescriptor.m_name = "Shader Variant Asset Builder"; // Both "Shader Variant Asset Builder" and "Shader Asset Builder" produce ShaderVariantAsset products. If you update // ShaderVariantAsset you will need to update BOTH version numbers, not just "Shader Variant Asset Builder". - shaderVariantAssetBuilderDescriptor.m_version = 25; // ATOM-15871 + shaderVariantAssetBuilderDescriptor.m_version = 26; // [AZSL] Changing inlineConstant to rootConstant keyword work. shaderVariantAssetBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern(AZStd::string::format("*.%s", RPI::ShaderVariantListSourceData::Extension), AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard)); shaderVariantAssetBuilderDescriptor.m_busId = azrtti_typeid(); shaderVariantAssetBuilderDescriptor.m_createJobFunction = AZStd::bind(&ShaderVariantAssetBuilder::CreateJobs, &m_shaderVariantAssetBuilder, AZStd::placeholders::_1, AZStd::placeholders::_2); diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderAssetBuilder.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderAssetBuilder.cpp index 3bc63b89a0..a0205efa72 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderAssetBuilder.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderAssetBuilder.cpp @@ -151,11 +151,11 @@ namespace AZ void ShaderAssetBuilder::CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) const { - AZStd::string fullPath; - AzFramework::StringFunc::Path::ConstructFull(request.m_watchFolder.data(), request.m_sourceFile.data(), fullPath, true); + AZStd::string shaderAssetSourceFileFullPath; + AzFramework::StringFunc::Path::ConstructFull(request.m_watchFolder.data(), request.m_sourceFile.data(), shaderAssetSourceFileFullPath, true); ShaderBuilderUtility::IncludedFilesParser includedFilesParser; - AZ_TracePrintf(ShaderAssetBuilderName, "CreateJobs for Shader \"%s\"\n", fullPath.data()); + AZ_TracePrintf(ShaderAssetBuilderName, "CreateJobs for Shader \"%s\"\n", shaderAssetSourceFileFullPath.data()); // Used to synchronize versions of the ShaderAsset and ShaderVariantTreeAsset, especially during hot-reload. // Note it's probably important for this to be set once outside the platform loop so every platform's ShaderAsset @@ -166,7 +166,7 @@ namespace AZ // Need to get the name of the azsl file from the .shader source asset, to be able to declare a dependency to SRG Layout Job. // and the macro options to preprocess. - auto descriptorParseOutcome = ShaderBuilderUtility::LoadShaderDataJson(fullPath); + auto descriptorParseOutcome = ShaderBuilderUtility::LoadShaderDataJson(shaderAssetSourceFileFullPath); if (!descriptorParseOutcome.IsSuccess()) { AZ_Error( @@ -178,7 +178,7 @@ namespace AZ RPI::ShaderSourceData shaderSourceData = descriptorParseOutcome.TakeValue(); AZStd::string azslFullPath; - ShaderBuilderUtility::GetAbsolutePathToAzslFile(fullPath, shaderSourceData.m_source, azslFullPath); + ShaderBuilderUtility::GetAbsolutePathToAzslFile(shaderAssetSourceFileFullPath, shaderSourceData.m_source, azslFullPath); { // Add the AZSL as source dependency @@ -191,9 +191,9 @@ namespace AZ { AZ_Error( ShaderAssetBuilderName, false, "Shader program listed as the source entry does not exist: %s.", azslFullPath.c_str()); - // Treat as success, so when the azsl file shows up the AP will try to recompile. - response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success; - return; + // Even though there was an error here, don't stop, because we need to report the SourceFileDependency so when the azsl + // file shows up the AP will try to recompile. We will go ahead and create the job anyway, and then ProcessJob can + // report the failure. } GlobalBuildOptions buildOptions = ReadBuildOptions(ShaderAssetBuilderName); @@ -229,7 +229,7 @@ namespace AZ } // for all request.m_enabledPlatforms AZ_TracePrintf( - ShaderAssetBuilderName, "CreateJobs for %s took %llu microseconds", fullPath.c_str(), + ShaderAssetBuilderName, "CreateJobs for %s took %llu microseconds", shaderAssetSourceFileFullPath.c_str(), AZStd::GetTimeNowMicroSecond() - shaderAssetBuildTimestamp); response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success; diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.cpp index e9ac18a432..e6c99630b2 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.cpp @@ -70,11 +70,11 @@ namespace AZ return AZ::Success(shaderSourceData); } - void GetAbsolutePathToAzslFile(const AZStd::string& shaderTemplatePathAndFile, AZStd::string specifiedShaderPathAndName, AZStd::string& absoluteAzslPath) + void GetAbsolutePathToAzslFile(const AZStd::string& shaderSourceFileFullPath, AZStd::string specifiedShaderPathAndName, AZStd::string& absoluteAzslPath) { AZStd::string sourcePath; - AzFramework::StringFunc::Path::GetFullPath(shaderTemplatePathAndFile.data(), sourcePath); + AzFramework::StringFunc::Path::GetFullPath(shaderSourceFileFullPath.c_str(), sourcePath); AzFramework::StringFunc::Path::Normalize(specifiedShaderPathAndName); bool shaderNameHasPath = (specifiedShaderPathAndName.find(AZ_CORRECT_FILESYSTEM_SEPARATOR) != AZStd::string::npos); @@ -82,15 +82,27 @@ namespace AZ // Join will handle overlapping directory structures for us AzFramework::StringFunc::Path::Join(sourcePath.data(), specifiedShaderPathAndName.data(), absoluteAzslPath, shaderNameHasPath /* handle directory overlap? */, false /* be case insensitive? */); - AzFramework::StringFunc::Path::ReplaceExtension(absoluteAzslPath, "azsl"); + // The builders used to automatically set the ".azsl" extension, but no more, because that would make the .shader file confusing to read. + // Here we just detect the issue and instruct the user what to change. + // (There's no need to return a failure code, the builder will eventually fail anyway when it can't find the file). + if (!IO::FileIOBase::GetInstance()->Exists(absoluteAzslPath.c_str())) + { + AZStd::string absoluteAzslPathWithForcedExtension = absoluteAzslPath; + AzFramework::StringFunc::Path::ReplaceExtension(absoluteAzslPathWithForcedExtension, "azsl"); + + if (IO::FileIOBase::GetInstance()->Exists(absoluteAzslPathWithForcedExtension.c_str())) + { + AZ_Error(ShaderBuilderUtilityName, false, "When the .shader file references a .azsl file, it must include the \".azsl\" extension."); + } + } } AZStd::shared_ptr PrepareSourceInput( [[maybe_unused]] const char* builderName, - const AZStd::string& shaderAssetSourcePath, + const AZStd::string& shaderSourceFileFullPath, RPI::ShaderSourceData& sourceAsset) { - auto shaderAssetSourceFileParseOutput = ShaderBuilderUtility::LoadShaderDataJson(shaderAssetSourcePath); + auto shaderAssetSourceFileParseOutput = ShaderBuilderUtility::LoadShaderDataJson(shaderSourceFileFullPath); if (!shaderAssetSourceFileParseOutput.IsSuccess()) { AZ_Error(builderName, false, "Failed to load/parse Shader Descriptor JSON: %s", shaderAssetSourceFileParseOutput.GetError().c_str()); @@ -100,7 +112,7 @@ namespace AZ AZStd::shared_ptr files(new ShaderFiles); const AZStd::string& specifiedAzslName = sourceAsset.m_source; - ShaderBuilderUtility::GetAbsolutePathToAzslFile(shaderAssetSourcePath, specifiedAzslName, files->m_azslSourceFullPath); + ShaderBuilderUtility::GetAbsolutePathToAzslFile(shaderSourceFileFullPath, specifiedAzslName, files->m_azslSourceFullPath); // specifiedAzslName may have a relative path on it so need to strip it AzFramework::StringFunc::Path::GetFileName(specifiedAzslName.c_str(), files->m_azslFileName); @@ -183,7 +195,7 @@ namespace AZ // access the root constants reflection if (!azslc.ParseSrgPopulateRootConstantData( outcomes[AzslSubProducts::srg].GetValue(), - rootConstantData)) // consuming data from --srg ("InlineConstantBuffer" subjson section) + rootConstantData)) // consuming data from --srg ("RootConstantBuffer" subjson section) { AZ_Error(builderName, false, "Failed to obtain root constant data reflection"); return AssetBuilderSDK::ProcessJobResult_Failed; diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.h b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.h index 5d45ade9cb..a27eddf2cc 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.h +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.h @@ -33,12 +33,12 @@ namespace AZ { Outcome LoadShaderDataJson(const AZStd::string& fullPathToJsonFile); - void GetAbsolutePathToAzslFile(const AZStd::string& shaderTemplatePathAndFile, AZStd::string specifiedShaderPathAndName, AZStd::string& absoluteShaderPath); + void GetAbsolutePathToAzslFile(const AZStd::string& shaderSourceFileFullPath, AZStd::string specifiedShaderPathAndName, AZStd::string& absoluteShaderPath); //! Opens and read the .shader, returns expanded file paths AZStd::shared_ptr PrepareSourceInput( const char* builderName, - const AZStd::string& shaderAssetSourcePath, + const AZStd::string& shaderSourceFileFullPath, RPI::ShaderSourceData& sourceAsset); namespace AzslSubProducts diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp index 27ae90dcdc..bb40baca7d 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp @@ -561,7 +561,7 @@ namespace AZ // Access the root constants reflection if (!azslCompiler.ParseSrgPopulateRootConstantData( jsonOutcome.GetValue(), - rootConstantData)) // consuming data from --srg ("InlineConstantBuffer" subjson section) + rootConstantData)) // consuming data from --srg ("RootConstantBuffer" subjson section) { AZ_Error(ShaderVariantAssetBuilderName, false, "Failed to obtain root constant data reflection"); return false; diff --git a/Gems/Atom/Feature/Common/Assets/Materials/ReflectionProbe/ReflectionProbeVisualization.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/ReflectionProbe/ReflectionProbeVisualization.materialtype index 17209771e5..30062205a8 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/ReflectionProbe/ReflectionProbeVisualization.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/ReflectionProbe/ReflectionProbeVisualization.materialtype @@ -1,7 +1,7 @@ { "description": "Base material for the reflection probe visualization model.", + "version": 1, "propertyLayout": { - "version": 1, "properties": { "general": [ { diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.azsl index 69f86cbe9f..1fc8a410a1 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.azsl +++ b/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.azsl @@ -54,6 +54,7 @@ VSOutput ShadowCatcherVS(VSInput IN) DirectionalLightShadow::GetShadowCoords( ViewSrg::m_shadowIndexDirectionalLight, worldPosition, + OUT.m_worldNormal, OUT.m_shadowCoords); return OUT; diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.materialtype index 74246f85db..d05f03c9a9 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.materialtype @@ -1,7 +1,7 @@ { "description": "Base material for the reflection probe visualization model.", + "version": 1, "propertyLayout": { - "version": 1, "properties": { "settings": [ { diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype index bed3b69c4c..f4bcfb2673 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype @@ -1,7 +1,7 @@ { "description": "Material Type with properties used to define Enhanced PBR, a metallic-roughness Physically-Based Rendering (PBR) material shading model, with advanced features like subsurface scattering, transmission, and anisotropy.", + "version": 3, "propertyLayout": { - "version": 3, "groups": [ { "name": "baseColor", diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR_ForwardPass.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR_ForwardPass.azsl index 11859de7d6..0f9771e480 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR_ForwardPass.azsl +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR_ForwardPass.azsl @@ -112,13 +112,15 @@ VSOutput EnhancedPbr_ForwardPassVS(VSInput IN) PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float depth) { + const float3 vertexNormal = normalize(IN.m_normal); + // ------- Tangents & Bitangets ------- float3 tangents[UvSetCount] = { IN.m_tangent.xyz, IN.m_tangent.xyz }; float3 bitangents[UvSetCount] = { IN.m_bitangent.xyz, IN.m_bitangent.xyz }; if ((o_parallax_feature_enabled && !o_enableSubsurfaceScattering) || o_normal_useTexture || (o_clearCoat_enabled && o_clearCoat_normal_useTexture) || o_detail_normal_useTexture) { - PrepareGeneratedTangent(IN.m_normal, IN.m_worldPosition, isFrontFace, IN.m_uv, UvSetCount, tangents, bitangents); + PrepareGeneratedTangent(vertexNormal, IN.m_worldPosition, isFrontFace, IN.m_uv, UvSetCount, tangents, bitangents); } // ------- Depth & Parallax ------- @@ -137,7 +139,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float float3x3 uvMatrix = MaterialSrg::m_parallaxUvIndex == 0 ? MaterialSrg::m_uvMatrix : CreateIdentity3x3(); float3x3 uvMatrixInverse = MaterialSrg::m_parallaxUvIndex == 0 ? MaterialSrg::m_uvMatrixInverse : CreateIdentity3x3(); - GetParallaxInput(IN.m_normal, tangents[MaterialSrg::m_parallaxUvIndex], bitangents[MaterialSrg::m_parallaxUvIndex], MaterialSrg::m_heightmapScale, MaterialSrg::m_heightmapOffset, + GetParallaxInput(vertexNormal, tangents[MaterialSrg::m_parallaxUvIndex], bitangents[MaterialSrg::m_parallaxUvIndex], MaterialSrg::m_heightmapScale, MaterialSrg::m_heightmapOffset, ObjectSrg::GetWorldMatrix(), uvMatrix, uvMatrixInverse, IN.m_uv[MaterialSrg::m_parallaxUvIndex], IN.m_worldPosition, depth, IN.m_position.w, displacementIsClipped); @@ -150,7 +152,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight; if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount) { - DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, IN.m_shadowCoords); + DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, vertexNormal, IN.m_shadowCoords); } } } @@ -185,7 +187,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float float2 normalUv = IN.m_uv[MaterialSrg::m_normalMapUvIndex]; float3x3 uvMatrix = MaterialSrg::m_normalMapUvIndex == 0 ? MaterialSrg::m_uvMatrix : CreateIdentity3x3(); // By design, only UV0 is allowed to apply transforms. float detailLayerNormalFactor = MaterialSrg::m_detail_normal_factor * detailLayerBlendFactor; - surface.vertexNormal = normalize(IN.m_normal); + surface.vertexNormal = vertexNormal; surface.normal = GetDetailedNormalInputWS( isFrontFace, IN.m_normal, tangents[MaterialSrg::m_normalMapUvIndex], bitangents[MaterialSrg::m_normalMapUvIndex], MaterialSrg::m_normalMap, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_normalFactor, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, uvMatrix, o_normal_useTexture, diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype index e4da9c6022..e2a05aa916 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype @@ -1,7 +1,7 @@ { "description": "Material Type tailored for rendering skin, with support for blended wrinkle maps that work with animated vertex blend shapes.", + "version": 3, "propertyLayout": { - "version": 3, "groups": [ { "name": "baseColor", diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype index 5fa0dcb217..107b525ae4 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype @@ -1,7 +1,7 @@ { "description": "Similar to StandardPBR but supports multiple layers blended together.", + "version": 3, "propertyLayout": { - "version": 3, "groups": [ { "name": "blend", diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.azsl index 6a97c0e785..a53dab7a01 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.azsl +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.azsl @@ -302,6 +302,7 @@ ProcessedMaterialInputs ProcessStandardMaterialInputs(StandardMaterialInputs inp PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float depthNDC) { + const float3 vertexNormal = normalize(IN.m_normal); depthNDC = IN.m_position.z; s_blendMaskFromVertexStream = IN.m_blendMask; @@ -321,7 +322,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float || o_layer3_o_clearCoat_normal_useTexture ) { - PrepareGeneratedTangent(IN.m_normal, IN.m_worldPosition, isFrontFace, IN.m_uv, UvSetCount, tangents, bitangents); + PrepareGeneratedTangent(vertexNormal, IN.m_worldPosition, isFrontFace, IN.m_uv, UvSetCount, tangents, bitangents); } // ------- Debug Modes ------- @@ -368,7 +369,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight; if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount) { - DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, IN.m_shadowCoords); + DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, vertexNormal, IN.m_shadowCoords); } } } @@ -445,7 +446,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float normalTS = ReorientTangentSpaceNormal(normalTS, lightingInputLayer3.m_normalTS); } // [GFX TODO][ATOM-14591]: This will only work if the normal maps all use the same UV stream. We would need to add support for having them in different UV streams. - surface.vertexNormal = normalize(IN.m_normal); + surface.vertexNormal = vertexNormal; surface.normal = normalize(TangentSpaceToWorld(normalTS, IN.m_normal, tangents[MaterialSrg::m_parallaxUvIndex], bitangents[MaterialSrg::m_parallaxUvIndex])); // ------- Combine Albedo, roughness, specular, roughness --------- diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.shader b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.shader index 28322d68ed..00efca6056 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.shader +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.shader @@ -48,6 +48,14 @@ } ] }, + + "Supervariants": + [ + { + "Name": "", + "PlusArguments": "--no-alignment-validation" + } + ], "DrawList" : "forward" } diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass_EDS.shader b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass_EDS.shader index 42366d6067..983245ffb0 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass_EDS.shader +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass_EDS.shader @@ -49,5 +49,13 @@ ] }, + "Supervariants": + [ + { + "Name": "", + "PlusArguments": "--no-alignment-validation" + } + ], + "DrawList" : "forward" } diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype index 6eb82b85ae..7527a7658a 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype @@ -1,7 +1,7 @@ { "description": "Material Type with properties used to define Standard PBR, a metallic-roughness Physically-Based Rendering (PBR) material shading model.", + "version": 3, "propertyLayout": { - "version": 3, "groups": [ { "name": "baseColor", diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl index 8df5e1ba56..1d5e4ad9e3 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl @@ -98,6 +98,8 @@ VSOutput StandardPbr_ForwardPassVS(VSInput IN) PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float depthNDC) { + const float3 vertexNormal = normalize(IN.m_normal); + // ------- Tangents & Bitangets ------- float3 tangents[UvSetCount] = { IN.m_tangent.xyz, IN.m_tangent.xyz }; float3 bitangents[UvSetCount] = { IN.m_bitangent.xyz, IN.m_bitangent.xyz }; @@ -128,7 +130,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight; if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount) { - DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, IN.m_shadowCoords); + DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, vertexNormal, IN.m_shadowCoords); } } } @@ -146,7 +148,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float float2 normalUv = IN.m_uv[MaterialSrg::m_normalMapUvIndex]; float3x3 uvMatrix = MaterialSrg::m_normalMapUvIndex == 0 ? MaterialSrg::m_uvMatrix : CreateIdentity3x3(); // By design, only UV0 is allowed to apply transforms. - surface.vertexNormal = normalize(IN.m_normal); + surface.vertexNormal = vertexNormal; surface.normal = GetNormalInputWS(MaterialSrg::m_normalMap, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, isFrontFace, IN.m_normal, tangents[MaterialSrg::m_normalMapUvIndex], bitangents[MaterialSrg::m_normalMapUvIndex], uvMatrix, o_normal_useTexture, MaterialSrg::m_normalFactor); diff --git a/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfField.pass b/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfField.pass new file mode 100644 index 0000000000..70cebab5a1 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfField.pass @@ -0,0 +1,170 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "NewDepthOfFieldTemplate", + "PassClass": "NewDepthOfFieldParentPass", + "Slots": [ + { + "Name": "Depth", + "SlotType": "Input" + }, + { + "Name": "LightingBuffer", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "RenderTarget" + } + ], + "PassRequests": [ + { + "Name": "AutoFocus", + "TemplateName": "DepthOfFieldReadBackFocusDepthTemplate", + "Connections": [ + { + "LocalSlot": "DepthInput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "Depth" + } + } + ] + }, + { + "Name": "Downsample", + "TemplateName": "NewDepthOfFieldDownsampleTemplate", + + "Connections": [ + { + "LocalSlot": "ColorInput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "LightingBuffer" + } + }, + { + "LocalSlot": "DepthInput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "Depth" + } + } + ] + }, + { + "Name": "TileReduce", + "TemplateName": "NewDepthOfFieldTileReduceTemplate", + + "Connections": [ + { + "LocalSlot": "ColorAndCocInput", + "AttachmentRef": { + "Pass": "Downsample", + "Attachment": "OutputColorAndCoC" + } + } + ] + }, + { + "Name": "Tile3x3", + "TemplateName": "NewDepthOfFieldTile3x3Template", + + "Connections": [ + { + "LocalSlot": "Input", + "AttachmentRef": { + "Pass": "TileReduce", + "Attachment": "MinMaxCoC" + } + } + ] + }, + { + "Name": "Tile5x5", + "TemplateName": "NewDepthOfFieldTile3x3Template", + + "Connections": [ + { + "LocalSlot": "Input", + "AttachmentRef": { + "Pass": "Tile3x3", + "Attachment": "Output" + } + } + ] + }, + { + "Name": "LargeFilter", + "TemplateName": "NewDepthOfFieldFilterLargeTemplate", + + "Connections": [ + { + "LocalSlot": "ColorAndCoc", + "AttachmentRef": { + "Pass": "Downsample", + "Attachment": "OutputColorAndCoC" + } + }, + { + "LocalSlot": "CocTile", + "AttachmentRef": { + "Pass": "Tile5x5", + "Attachment": "Output" + } + } + ] + }, + { + "Name": "SmallFilter", + "TemplateName": "NewDepthOfFieldFilterSmallTemplate", + + "Connections": [ + { + "LocalSlot": "ColorAndCoc", + "AttachmentRef": { + "Pass": "LargeFilter", + "Attachment": "OutputColorAndCoc" + } + }, + { + "LocalSlot": "CocTile", + "AttachmentRef": { + "Pass": "Tile3x3", + "Attachment": "Output" + } + } + ] + }, + { + "Name": "Composite", + "TemplateName": "NewDepthOfFieldCompositeTemplate", + + "Connections": [ + { + "LocalSlot": "Depth", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "Depth" + } + }, + { + "LocalSlot": "HalfResColorAndCoC", + "AttachmentRef": { + "Pass": "SmallFilter", + "Attachment": "OutputColorAndCoc" + } + }, + { + "LocalSlot": "ColorInputOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "LightingBuffer" + } + } + ] + } + ] + } + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldComposite.pass b/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldComposite.pass new file mode 100644 index 0000000000..522ae78fa5 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldComposite.pass @@ -0,0 +1,37 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "NewDepthOfFieldCompositeTemplate", + "PassClass": "FullScreenTriangle", + "Slots": [ + { + "Name": "Depth", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderImageDimensionsConstant": "m_fullResDimensions" + }, + { + "Name": "HalfResColorAndCoC", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderImageDimensionsConstant": "m_halfResDimensions" + }, + { + "Name": "ColorInputOutput", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "RenderTarget" + } + ], + "PassData": { + "$type": "FullscreenTrianglePassData", + "ShaderAsset": { + "FilePath": "Shaders/PostProcessing/NewDepthOfFieldComposite.shader" + }, + "PipelineViewTag": "MainCamera" + } + } + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldDownsample.pass b/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldDownsample.pass new file mode 100644 index 0000000000..2f1787c6cd --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldDownsample.pass @@ -0,0 +1,72 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "NewDepthOfFieldDownsampleTemplate", + "PassClass": "FullScreenTriangle", + "Slots": [ + { + "Name": "ColorInput", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderImageDimensionsConstant": "m_inputDimensions" + }, + { + "Name": "DepthInput", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "AspectFlags": [ + "Depth" + ] + } + }, + { + "Name": "OutputColorAndCoC", + "SlotType": "Output", + "ScopeAttachmentUsage": "RenderTarget", + "ShaderImageDimensionsConstant": "m_outputDimensions", + "LoadStoreAction": { + "LoadAction": "Clear" + } + } + ], + "ImageAttachments": [ + { + "Name": "OutputAttachment", + "SizeSource": { + "Source": { + "Pass": "This", + "Attachment": "ColorInput" + }, + "Multipliers": { + "WidthMultiplier": 0.5, + "HeightMultiplier": 0.5 + } + }, + "ImageDescriptor": { + "Format": "R16G16B16A16_FLOAT" + } + } + ], + "Connections": [ + { + "LocalSlot": "OutputColorAndCoC", + "AttachmentRef": { + "Pass": "This", + "Attachment": "OutputAttachment" + } + } + ], + "PassData": { + "$type": "FullscreenTrianglePassData", + "ShaderAsset": { + "FilePath": "Shaders/PostProcessing/NewDepthOfFieldDownsample.shader" + }, + "PipelineViewTag": "MainCamera" + } + } + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldFilterLarge.pass b/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldFilterLarge.pass new file mode 100644 index 0000000000..7b40009cec --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldFilterLarge.pass @@ -0,0 +1,62 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "NewDepthOfFieldFilterLargeTemplate", + "PassClass": "NewDepthOfFieldFilterPass", + "Slots": [ + { + "Name": "ColorAndCoc", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderImageDimensionsConstant": "m_textureDimensions" + }, + { + "Name": "CocTile", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { + "Name": "OutputColorAndCoc", + "SlotType": "Output", + "ScopeAttachmentUsage": "RenderTarget", + "LoadStoreAction": { + "LoadAction": "Clear" + } + } + ], + "ImageAttachments": [ + { + "Name": "OutputAttachment", + "SizeSource": { + "Source": { + "Pass": "This", + "Attachment": "ColorAndCoc" + } + }, + "ImageDescriptor": { + "Format": "R16G16B16A16_FLOAT" + } + } + ], + "Connections": [ + { + "LocalSlot": "OutputColorAndCoc", + "AttachmentRef": { + "Pass": "This", + "Attachment": "OutputAttachment" + } + } + ], + "PassData": { + "$type": "FullscreenTrianglePassData", + "ShaderAsset": { + "FilePath": "Shaders/PostProcessing/NewDepthOfFieldFilterLarge.shader" + }, + "PipelineViewTag": "MainCamera" + } + } + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldFilterSmall.pass b/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldFilterSmall.pass new file mode 100644 index 0000000000..35a658a3b1 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldFilterSmall.pass @@ -0,0 +1,62 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "NewDepthOfFieldFilterSmallTemplate", + "PassClass": "NewDepthOfFieldFilterPass", + "Slots": [ + { + "Name": "ColorAndCoc", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderImageDimensionsConstant": "m_textureDimensions" + }, + { + "Name": "CocTile", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { + "Name": "OutputColorAndCoc", + "SlotType": "Output", + "ScopeAttachmentUsage": "RenderTarget", + "LoadStoreAction": { + "LoadAction": "Clear" + } + } + ], + "ImageAttachments": [ + { + "Name": "OutputAttachment", + "SizeSource": { + "Source": { + "Pass": "This", + "Attachment": "ColorAndCoc" + } + }, + "ImageDescriptor": { + "Format": "R16G16B16A16_FLOAT" + } + } + ], + "Connections": [ + { + "LocalSlot": "OutputColorAndCoc", + "AttachmentRef": { + "Pass": "This", + "Attachment": "OutputAttachment" + } + } + ], + "PassData": { + "$type": "FullscreenTrianglePassData", + "ShaderAsset": { + "FilePath": "Shaders/PostProcessing/NewDepthOfFieldFilterSmall.shader" + }, + "PipelineViewTag": "MainCamera" + } + } + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldTile3x3.pass b/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldTile3x3.pass new file mode 100644 index 0000000000..93ae645c83 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldTile3x3.pass @@ -0,0 +1,57 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "NewDepthOfFieldTile3x3Template", + "PassClass": "FullScreenTriangle", + "Slots": [ + { + "Name": "Input", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderImageDimensionsConstant": "m_textureDimensions" + }, + { + "Name": "Output", + "SlotType": "Output", + "ScopeAttachmentUsage": "RenderTarget", + "LoadStoreAction": { + "LoadAction": "Clear" + } + } + ], + "ImageAttachments": [ + { + "Name": "OutputAttachment", + "SizeSource": { + "Source": { + "Pass": "This", + "Attachment": "Input" + } + }, + "ImageDescriptor": { + "Format": "R16G16_SNORM" + } + } + ], + "Connections": [ + { + "LocalSlot": "Output", + "AttachmentRef": { + "Pass": "This", + "Attachment": "OutputAttachment" + } + } + ], + "PassData": { + "$type": "FullscreenTrianglePassData", + "ShaderAsset": { + "FilePath": "Shaders/PostProcessing/NewDepthOfFieldTile3x3.shader" + }, + "PipelineViewTag": "MainCamera" + } + } + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldTileReduce.pass b/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldTileReduce.pass new file mode 100644 index 0000000000..2bcf0ef32d --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Passes/NewDepthOfFieldTileReduce.pass @@ -0,0 +1,63 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "NewDepthOfFieldTileReduceTemplate", + "PassClass": "NewDepthOfFieldTileReducePass", + "Slots": [ + { + "Name": "ColorAndCocInput", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderImageDimensionsConstant": "m_inputDimensions" + }, + { + "Name": "MinMaxCoC", + "SlotType": "Output", + "ScopeAttachmentUsage": "Shader", + "ShaderImageDimensionsConstant": "m_outputDimensions", + "LoadStoreAction": { + "LoadAction": "Clear" + } + } + ], + "ImageAttachments": [ + { + "Name": "MinMaxCoCAttachment", + "SizeSource": { + "Source": { + "Pass": "This", + "Attachment": "ColorAndCocInput" + }, + "Multipliers": { + // 1/16 = 0.0625 + "WidthMultiplier": 0.0625, + "HeightMultiplier": 0.0625 + } + }, + "ImageDescriptor": { + "Format": "R16G16_SNORM" + } + } + ], + "Connections": [ + { + "LocalSlot": "MinMaxCoC", + "AttachmentRef": { + "Pass": "This", + "Attachment": "MinMaxCoCAttachment" + } + } + ], + "PassData": { + "$type": "ComputePassData", + "ShaderAsset": { + "FilePath": "Shaders/PostProcessing/NewDepthOfFieldTileReduce.shader" + }, + "PipelineViewTag": "MainCamera" + } + } + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset b/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset index 7770b326a6..f2df085228 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset +++ b/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset @@ -185,6 +185,34 @@ "Path": "Passes/DepthOfFieldWriteFocusDepthFromGpu.pass" }, { + "Name": "NewDepthOfFieldTemplate", + "Path": "Passes/NewDepthOfField.pass" + }, + { + "Name": "NewDepthOfFieldDownsampleTemplate", + "Path": "Passes/NewDepthOfFieldDownsample.pass" + }, + { + "Name": "NewDepthOfFieldTileReduceTemplate", + "Path": "Passes/NewDepthOfFieldTileReduce.pass" + }, + { + "Name": "NewDepthOfFieldTile3x3Template", + "Path": "Passes/NewDepthOfFieldTile3x3.pass" + }, + { + "Name": "NewDepthOfFieldFilterLargeTemplate", + "Path": "Passes/NewDepthOfFieldFilterLarge.pass" + }, + { + "Name": "NewDepthOfFieldFilterSmallTemplate", + "Path": "Passes/NewDepthOfFieldFilterSmall.pass" + }, + { + "Name": "NewDepthOfFieldCompositeTemplate", + "Path": "Passes/NewDepthOfFieldComposite.pass" + }, + { "Name": "EsmShadowmapsTemplate", "Path": "Passes/EsmShadowmaps.pass" }, @@ -469,12 +497,12 @@ "Path": "Passes/OpaqueParent.pass" }, { - "Name": "ThumbnailPipeline", - "Path": "Passes/ThumbnailPipeline.pass" + "Name": "ToolsPipeline", + "Path": "Passes/ToolsPipeline.pass" }, { - "Name": "ThumbnailPipelineRenderToTexture", - "Path": "Passes/ThumbnailPipelineRenderToTexture.pass" + "Name": "ToolsPipelineRenderToTexture", + "Path": "Passes/ToolsPipelineRenderToTexture.pass" }, { "Name": "TransparentParentTemplate", diff --git a/Gems/Atom/Feature/Common/Assets/Passes/PostProcessParent.pass b/Gems/Atom/Feature/Common/Assets/Passes/PostProcessParent.pass index fb27770da3..67c5072513 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/PostProcessParent.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/PostProcessParent.pass @@ -112,20 +112,42 @@ } ] }, + // Todo: remove the old depth of field implementation and rename NewDepthOfField -> DepthOfField + //{ + // "Name": "DepthOfFieldPass", + // "TemplateName": "DepthOfFieldTemplate", + // "Enabled": true, + // "Connections": [ + // { + // "LocalSlot": "DoFColorInput", + // "AttachmentRef": { + // "Pass": "TaaPass", + // "Attachment": "OutputColor" + // } + // }, + // { + // "LocalSlot": "DoFDepthInput", + // "AttachmentRef": { + // "Pass": "Parent", + // "Attachment": "Depth" + // } + // } + // ] + //}, { "Name": "DepthOfFieldPass", - "TemplateName": "DepthOfFieldTemplate", + "TemplateName": "NewDepthOfFieldTemplate", "Enabled": true, "Connections": [ { - "LocalSlot": "DoFColorInput", + "LocalSlot": "LightingBuffer", "AttachmentRef": { "Pass": "TaaPass", "Attachment": "OutputColor" } }, { - "LocalSlot": "DoFDepthInput", + "LocalSlot": "Depth", "AttachmentRef": { "Pass": "Parent", "Attachment": "Depth" @@ -142,7 +164,7 @@ "LocalSlot": "InputOutput", "AttachmentRef": { "Pass": "DepthOfFieldPass", - "Attachment": "DoFOutput" + "Attachment": "LightingBuffer" } } ] diff --git a/Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipeline.pass b/Gems/Atom/Feature/Common/Assets/Passes/ToolsPipeline.pass similarity index 99% rename from Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipeline.pass rename to Gems/Atom/Feature/Common/Assets/Passes/ToolsPipeline.pass index 932b6ac435..51c169348b 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipeline.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/ToolsPipeline.pass @@ -4,7 +4,7 @@ "ClassName": "PassAsset", "ClassData": { "PassTemplate": { - "Name": "ThumbnailPipeline", + "Name": "ToolsPipeline", "PassClass": "ParentPass", "Slots": [ { diff --git a/Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipelineRenderToTexture.pass b/Gems/Atom/Feature/Common/Assets/Passes/ToolsPipelineRenderToTexture.pass similarity index 88% rename from Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipelineRenderToTexture.pass rename to Gems/Atom/Feature/Common/Assets/Passes/ToolsPipelineRenderToTexture.pass index 11e2cb717a..b98ec46d0e 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipelineRenderToTexture.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/ToolsPipelineRenderToTexture.pass @@ -4,7 +4,7 @@ "ClassName": "PassAsset", "ClassData": { "PassTemplate": { - "Name": "ThumbnailPipelineRenderToTexture", + "Name": "ToolsPipelineRenderToTexture", "PassClass": "RenderToTexturePass", "PassData": { "$type": "RenderToTexturePassData", @@ -15,7 +15,7 @@ "PassRequests": [ { "Name": "Pipeline", - "TemplateName": "ThumbnailPipeline", + "TemplateName": "ToolsPipeline", "Connections": [ { "LocalSlot": "SwapChainOutput", diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/3rdParty/Features/PostProcessing/KelvinToRgb.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/3rdParty/Features/PostProcessing/KelvinToRgb.azsli index aa57f0f4a1..1c8b77df45 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/3rdParty/Features/PostProcessing/KelvinToRgb.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/3rdParty/Features/PostProcessing/KelvinToRgb.azsli @@ -16,39 +16,6 @@ // licensed and released under Creative Commons 3.0 Attribution // https://creativecommons.org/licenses/by/3.0/ -float3 HueToRgb(float hue) -{ - return saturate(float3(abs(hue * 6.0f - 3.0f) - 1.0f, - 2.0f - abs(hue * 6.0f - 2.0f), - 2.0f - abs(hue * 6.0f - 4.0f))); -} - -float3 RgbToHcv(float3 rgb) -{ - // Based on work by Sam Hocevar and Emil Persson - const float4 p = (rgb.g < rgb.b) ? float4(rgb.bg, -1.0f, 2.0f/3.0f) : float4(rgb.gb, 0.0f, -1.0f/3.0f); - const float4 q1 = (rgb.r < p.x) ? float4(p.xyw, rgb.r) : float4(rgb.r, p.yzx); - const float c = q1.x - min(q1.w, q1.y); - const float h = abs((q1.w - q1.y) / (6.0f * c + 0.000001f ) + q1.z); - return float3(h, c, q1.x); -} - -float3 RgbToHsl(float3 rgb) -{ - rgb.xyz = max(rgb.xyz, 0.000001f); - const float3 hcv = RgbToHcv(rgb); - const float L = hcv.z - hcv.y * 0.5f; - const float S = hcv.y / (1.0f - abs(L * 2.0f - 1.0f) + 0.000001f); - return float3(hcv.x, S, L); -} - -float3 HslToRgb(float3 hsl) -{ - const float3 rgb = HueToRgb(hsl.x); - const float c = (1.0f - abs(2.0f * hsl.z - 1.0f)) * hsl.y; - return (rgb - 0.5f) * c + hsl.z; -} - // Color temperature float3 KelvinToRgb(float kelvin) { diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl index 099a6394d4..423d447f7b 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl @@ -66,12 +66,21 @@ float3 ColorGradeSaturation (float3 frameColor, float control) return (frameColor - vLuminance) * control + vLuminance; } -float3 ColorGradeKelvinColorTemp(float3 frameColor, float kelvin) +float3 ColorGradeWhiteBalance(float3 frameColor, float kelvin, float tint, float luminancePreservation) { const float3 kColor = TransformColor(KelvinToRgb(kelvin), ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg); const float luminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg); - const float3 resHsl = RgbToHsl(frameColor.rgb * kColor.rgb); // Apply Kelvin color and convert to HSL - return HslToRgb(float3(resHsl.xy, luminance)); // Preserve luminance + + // Apply Kelvin color and tint and calculate the new luminance + float3 adjustedColor = frameColor.rgb * kColor.rgb; + adjustedColor.g = max(0.0, adjustedColor.g + tint * 0.001); + const float adjustedLuminance = CalculateLuminance(adjustedColor, ColorSpaceId::ACEScg); + + // Adjust the color based on the difference in luminance. + const float luminanceDifferenceRatio = luminance / adjustedLuminance; + const float3 adjustedColorLumPreserved = adjustedColor * luminanceDifferenceRatio; + + return lerp(adjustedColor, adjustedColorLumPreserved, luminancePreservation); } // pow(f, e) won't work if f is negative, or may cause inf/NAN. @@ -132,13 +141,17 @@ float3 ColorGradeShadowsMidtonesHighlights (float3 frameColor, float shadowsStar float3 ColorGrade(float3 frameColor) { frameColor = lerp(frameColor, ColorGradePostExposure(frameColor, PassSrg::m_colorGradingExposure), PassSrg::m_colorAdjustmentWeight); - frameColor = lerp(frameColor, ColorGradeKelvinColorTemp(frameColor, PassSrg::m_whiteBalanceKelvin), PassSrg::m_whiteBalanceWeight); + frameColor = lerp(frameColor, ColorGradeWhiteBalance( + frameColor, PassSrg::m_whiteBalanceKelvin, + PassSrg::m_whiteBalanceTint, + PassSrg::m_whiteBalanceLuminancePreservation), + PassSrg::m_whiteBalanceWeight); frameColor = lerp(frameColor, ColorGradingContrast(frameColor, AcesCcMidGrey, PassSrg::m_colorGradingContrast), PassSrg::m_colorAdjustmentWeight); frameColor = lerp(frameColor, ColorGradeColorFilter(frameColor, PassSrg::m_colorFilterSwatch.rgb, PassSrg::m_colorFilterMultiply, PassSrg::m_colorFilterIntensity), PassSrg::m_colorAdjustmentWeight); frameColor = max(frameColor, 0.0); frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPreSaturation), PassSrg::m_colorAdjustmentWeight); - + frameColor = max(frameColor, 0.0); frameColor = ColorGradeSplitTone(frameColor, PassSrg::m_splitToneBalance, PassSrg::m_splitToneWeight, PassSrg::m_splitToneShadowsColor, PassSrg::m_splitToneHighlightsColor); frameColor = ColorGradeChannelMixer(frameColor, PassSrg::m_channelMixingRed, PassSrg::m_channelMixingGreen, PassSrg::m_channelMixingBlue); @@ -147,8 +160,7 @@ float3 ColorGrade(float3 frameColor) PassSrg::m_smhHighlightsStart, PassSrg::m_smhHighlightsEnd, PassSrg::m_smhWeight, PassSrg::m_smhShadowsColor, PassSrg::m_smhMidtonesColor, PassSrg::m_smhHighlightsColor); - - frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPostSaturation), PassSrg::m_finalAdjustmentWeight); frameColor = lerp(frameColor, ColorGradeHueShift(frameColor, PassSrg::m_colorGradingHueShift), PassSrg::m_finalAdjustmentWeight); + frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPostSaturation), PassSrg::m_finalAdjustmentWeight); return max(frameColor.rgb, 0.0); } diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSrgs.shader b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSrgs.shader index 5512b1ad4d..475a1b1b54 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSrgs.shader +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSrgs.shader @@ -37,7 +37,7 @@ [ { "Name": "", - "PlusArguments": "", + "PlusArguments": "--no-alignment-validation", "MinusArguments": "--strip-unused-srgs" } ] diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli index 633ea85387..a7122aaf3a 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli @@ -14,6 +14,7 @@ #include "ShadowmapAtlasLib.azsli" #include "BicubicPcfFilters.azsli" #include "ReceiverPlaneDepthBias.azsli" +#include "NormalOffsetShadows.azsli" // Before including this azsli file, a PassSrg must be defined with the following members: // Texture2DArray m_directionalLightShadowmap; @@ -45,6 +46,7 @@ class DirectionalLightShadow static void GetShadowCoords( uint lightIndex, float3 worldPosition, + float3 worldNormal, out float3 shadowCoords[ViewSrg::MaxCascadeCount]); //! This calculates visibility ratio of the surface from the light origin. @@ -109,18 +111,24 @@ class DirectionalLightShadow void DirectionalLightShadow::GetShadowCoords( uint lightIndex, float3 worldPosition, + float3 worldNormal, out float3 shadowCoords[ViewSrg::MaxCascadeCount]) { - const float4x4 depthBiasMatrices[ViewSrg::MaxCascadeCount] = - ViewSrg::m_directionalLightShadows[lightIndex].m_depthBiasMatrices; + const float shadowBias = ViewSrg::m_directionalLightShadows[lightIndex].m_shadowBias; + + const float4x4 lightViewToShadowmapMatrices[ViewSrg::MaxCascadeCount] = ViewSrg::m_directionalLightShadows[lightIndex].m_lightViewToShadowmapMatrices; + const float4x4 worldToLightViewMatrices[ViewSrg::MaxCascadeCount] = ViewSrg::m_directionalLightShadows[lightIndex].m_worldToLightViewMatrices; + const uint cascadeCount = ViewSrg::m_directionalLightShadows[lightIndex].m_cascadeCount; + const float3 shadowOffset = ComputeNormalShadowOffset(ViewSrg::m_directionalLightShadows[lightIndex].m_normalShadowBias, worldNormal, ViewSrg::m_directionalLightShadows[lightIndex].m_shadowmapSize); for (uint index = 0; index < cascadeCount; ++index) - { - const float4x4 depthBiasMatrix = depthBiasMatrices[index]; - const float4 shadowCoordHomogeneous = mul(depthBiasMatrix, - float4(worldPosition, 1.)); - shadowCoords[index] = shadowCoordHomogeneous.xyz / shadowCoordHomogeneous.w; + { + float4 lightSpacePos = mul(worldToLightViewMatrices[index], float4(worldPosition + shadowOffset, 1.)); + lightSpacePos.z += shadowBias; + + const float4 clipSpacePos = mul(lightViewToShadowmapMatrices[index], lightSpacePos); + shadowCoords[index] = clipSpacePos.xyz / clipSpacePos.w; } } diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/NormalOffsetShadows.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/NormalOffsetShadows.azsli new file mode 100644 index 0000000000..1e78cec89e --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/NormalOffsetShadows.azsli @@ -0,0 +1,22 @@ +/* + * 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 + +// Helper functions for normal offset shadow mapping. +// Normal Offset is an alternative to slope-scale depth bias. +// We bias the shadow map lookup by transforming the world-position along the geometric normal before hand. +// http://web.archive.org/web/20140810230446/https://www.dissidentlogic.com/old/#Normal%20Offset%20Shadows +// + +// Apply the following to the world position. Then use this modified world position to look up in the shadow map +float3 ComputeNormalShadowOffset(const float normalOffsetBias, const float3 worldNormal, const float shadowMapDimension) +{ + const float shadowmapSize = 2.0f / shadowMapDimension; + return float3(worldNormal * normalOffsetBias * shadowmapSize); +} diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Vertex/VertexHelper.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Vertex/VertexHelper.azsli index 4b5047d4c3..c93986cea1 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Vertex/VertexHelper.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Vertex/VertexHelper.azsli @@ -47,6 +47,7 @@ void VertexHelper(in VSInput IN, inout VSOutput OUT, float3 worldPosition, bool DirectionalLightShadow::GetShadowCoords( shadowIndex, worldPosition, + OUT.m_normal, OUT.m_shadowCoords); } } diff --git a/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/CoreLights/ViewSrg.azsli b/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/CoreLights/ViewSrg.azsli index 94d6f20da3..b3bcc186b6 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/CoreLights/ViewSrg.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/CoreLights/ViewSrg.azsli @@ -81,10 +81,10 @@ partial ShaderResourceGroup ViewSrg uint m_shadowmapArraySlice; // array slice who has shadowmap in the atlas. uint m_shadowFilterMethod; float m_boundaryScale; - uint m_predictionSampleCount; uint m_filteringSampleCount; float2 m_unprojectConstants; float m_bias; + float m_normalShadowBias; float m_esmExponent; float3 m_padding; }; @@ -102,18 +102,19 @@ partial ShaderResourceGroup ViewSrg struct DirectionalLightShadow { - float4x4 m_depthBiasMatrices[MaxCascadeCount]; float4x4 m_lightViewToShadowmapMatrices[MaxCascadeCount]; float4x4 m_worldToLightViewMatrices[MaxCascadeCount]; float m_slopeBiasBase[MaxCascadeCount]; float m_boundaryScale; uint m_shadowmapSize; // width and height of shadowmap uint m_cascadeCount; - uint m_predictionSampleCount; + float m_shadowBias; + float m_normalShadowBias; uint m_filteringSampleCount; uint m_debugFlags; uint m_shadowFilterMethod; float m_far_minus_near; + float3 m_padding; }; enum ShadowFilterMethod diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/AuxGeom/AuxGeomObject.shader b/Gems/Atom/Feature/Common/Assets/Shaders/AuxGeom/AuxGeomObject.shader index 88120b3a9d..83012fe13c 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/AuxGeom/AuxGeomObject.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/AuxGeom/AuxGeomObject.shader @@ -1,5 +1,5 @@ { - "Source" : "AuxGeomObject", + "Source" : "AuxGeomObject.azsl", "DepthStencilState" : { "Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/AuxGeom/AuxGeomObjectLit.shader b/Gems/Atom/Feature/Common/Assets/Shaders/AuxGeom/AuxGeomObjectLit.shader index 98ace1da32..b06a0cd662 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/AuxGeom/AuxGeomObjectLit.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/AuxGeom/AuxGeomObjectLit.shader @@ -1,5 +1,5 @@ { - "Source" : "AuxGeomObjectLit", + "Source" : "AuxGeomObjectLit.azsl", "DepthStencilState" : { "Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/AuxGeom/AuxGeomWorld.shader b/Gems/Atom/Feature/Common/Assets/Shaders/AuxGeom/AuxGeomWorld.shader index ad49848f0f..2d7ff39999 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/AuxGeom/AuxGeomWorld.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/AuxGeom/AuxGeomWorld.shader @@ -1,5 +1,5 @@ { - "Source" : "AuxGeomWorld", + "Source" : "AuxGeomWorld.azsl", "DepthStencilState" : { "Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/BRDFTexture/BRDFTextureCS.shader b/Gems/Atom/Feature/Common/Assets/Shaders/BRDFTexture/BRDFTextureCS.shader index 50ce945edd..376e2bb5a3 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/BRDFTexture/BRDFTextureCS.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/BRDFTexture/BRDFTextureCS.shader @@ -1,5 +1,5 @@ { - "Source": "BRDFTextureCS", + "Source": "BRDFTextureCS.azsl", "ProgramSettings": { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Checkerboard/CheckerboardColorResolveCS.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Checkerboard/CheckerboardColorResolveCS.shader index f21f3e5c01..c08065a550 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Checkerboard/CheckerboardColorResolveCS.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Checkerboard/CheckerboardColorResolveCS.shader @@ -1,5 +1,5 @@ { - "Source": "CheckerboardColorResolveCS", + "Source": "CheckerboardColorResolveCS.azsl", "CompilerHints": { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/ColorGrading/LutGeneration.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/ColorGrading/LutGeneration.azsl index 6a76f58519..89655a0e91 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/ColorGrading/LutGeneration.azsl +++ b/Gems/Atom/Feature/Common/Assets/Shaders/ColorGrading/LutGeneration.azsl @@ -57,6 +57,7 @@ ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback float m_whiteBalanceWeight; float m_whiteBalanceKelvin; float m_whiteBalanceTint; + float m_whiteBalanceLuminancePreservation; float m_splitToneBalance; float m_splitToneWeight; diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/ColorGrading/LutGeneration.shader b/Gems/Atom/Feature/Common/Assets/Shaders/ColorGrading/LutGeneration.shader index 4e2c83db35..9c7e66fbfd 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/ColorGrading/LutGeneration.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/ColorGrading/LutGeneration.shader @@ -1,5 +1,5 @@ { - "Source" : "LutGeneration", + "Source" : "LutGeneration.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPassSkin.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPassSkin.shader index de6c989223..849c7e3441 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPassSkin.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Depth/DepthPassSkin.shader @@ -1,5 +1,5 @@ { - "Source" : "DepthPassSkin", + "Source" : "DepthPassSkin.azsl", "DepthStencilState" : { "Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/DiffuseComposite.shader b/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/DiffuseComposite.shader index 1593f0bb66..ba145cd7cf 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/DiffuseComposite.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/DiffuseComposite.shader @@ -1,5 +1,5 @@ { - "Source" : "DiffuseComposite", + "Source" : "DiffuseComposite.azsl", "RasterState" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/DiffuseGlobalFullscreen.shader b/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/DiffuseGlobalFullscreen.shader index 3d4b711f63..1279775ad0 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/DiffuseGlobalFullscreen.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/DiffuseGlobalFullscreen.shader @@ -1,5 +1,5 @@ { - "Source" : "DiffuseGlobalFullscreen", + "Source" : "DiffuseGlobalFullscreen.azsl", "RasterState" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/DiffuseProbeGridDownsample.shader b/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/DiffuseProbeGridDownsample.shader index dff0f755d0..c3494e6bea 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/DiffuseProbeGridDownsample.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/DiffuseProbeGridDownsample.shader @@ -1,5 +1,5 @@ { - "Source" : "DiffuseProbeGridDownsample", + "Source" : "DiffuseProbeGridDownsample.azsl", "RasterState" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/ImGui/ImGui.shader b/Gems/Atom/Feature/Common/Assets/Shaders/ImGui/ImGui.shader index 4e742f8194..46729ff7f2 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/ImGui/ImGui.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/ImGui/ImGui.shader @@ -1,6 +1,6 @@ { - "Source" : "ImGui", + "Source" : "ImGui.azsl", "RasterState" : { "CullMode" : "None" }, diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCulling.shader b/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCulling.shader index 6a4adcaade..24c1184b53 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCulling.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCulling.shader @@ -1,5 +1,5 @@ { - "Source": "LightCulling", + "Source": "LightCulling.azsl", "CompilerHints": { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCullingHeatmap.shader b/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCullingHeatmap.shader index 99b6c314fb..65e6a205e4 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCullingHeatmap.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCullingHeatmap.shader @@ -1,5 +1,5 @@ { - "Source" : "LightCullingHeatmap", + "Source" : "LightCullingHeatmap.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCullingRemap.shader b/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCullingRemap.shader index d1b52525b6..795f028eba 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCullingRemap.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCullingRemap.shader @@ -1,5 +1,5 @@ { - "Source": "LightCullingRemap", + "Source": "LightCullingRemap.azsl", "Compiler": { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCullingTilePrepare.shader b/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCullingTilePrepare.shader index 070bc8e2bc..6a3173da96 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCullingTilePrepare.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/LightCulling/LightCullingTilePrepare.shader @@ -1,5 +1,5 @@ { - "Source": "LightCullingTilePrepare", + "Source": "LightCullingTilePrepare.azsl", "CompilerHints": { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/LuxCore/RenderTexture.shader b/Gems/Atom/Feature/Common/Assets/Shaders/LuxCore/RenderTexture.shader index 32fff758be..6035f98f5d 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/LuxCore/RenderTexture.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/LuxCore/RenderTexture.shader @@ -1,5 +1,5 @@ { - "Source" : "RenderTexture", + "Source" : "RenderTexture.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MorphTargets/MorphTargetCS.shader b/Gems/Atom/Feature/Common/Assets/Shaders/MorphTargets/MorphTargetCS.shader index 08b1e7c298..38baa9ab87 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/MorphTargets/MorphTargetCS.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/MorphTargets/MorphTargetCS.shader @@ -1,5 +1,5 @@ { - "Source": "MorphTargetCS", + "Source": "MorphTargetCS.azsl", "ProgramSettings": { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/CameraMotionVector.shader b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/CameraMotionVector.shader index 4dee7cc702..f4a92a84d7 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/CameraMotionVector.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/CameraMotionVector.shader @@ -1,5 +1,5 @@ { - "Source": "CameraMotionVector", + "Source": "CameraMotionVector.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVector.shader b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVector.shader index c585060f3d..2badc21957 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVector.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVector.shader @@ -1,5 +1,5 @@ { - "Source" : "MeshMotionVector", + "Source" : "MeshMotionVector.azsl", "DepthStencilState" : { "Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorSkin.shader b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorSkin.shader index 9a50e4e2cf..a253c2704b 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorSkin.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorSkin.shader @@ -1,5 +1,5 @@ { - "Source" : "MeshMotionVectorSkin", + "Source" : "MeshMotionVectorSkin.azsl", "DepthStencilState" : { "Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/AcesOutputTransformLut.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/AcesOutputTransformLut.shader index 69ffb44394..b0f3a81cc9 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/AcesOutputTransformLut.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/AcesOutputTransformLut.shader @@ -1,5 +1,5 @@ { - "Source" : "AcesOutputTransformLut", + "Source" : "AcesOutputTransformLut.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ApplyShaperLookupTable.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ApplyShaperLookupTable.shader index ec18e33e7d..328b4353d8 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ApplyShaperLookupTable.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ApplyShaperLookupTable.shader @@ -1,5 +1,5 @@ { - "Source" : "ApplyShaperLookupTable", + "Source" : "ApplyShaperLookupTable.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BakeAcesOutputTransformLutCS.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BakeAcesOutputTransformLutCS.shader index 9274fa701e..928360b5ab 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BakeAcesOutputTransformLutCS.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BakeAcesOutputTransformLutCS.shader @@ -1,5 +1,5 @@ { - "Source": "BakeAcesOutputTransformLutCS", + "Source": "BakeAcesOutputTransformLutCS.azsl", "ProgramSettings": { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BlendColorGradingLuts.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BlendColorGradingLuts.shader index 10bccf1d42..ed647d8398 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BlendColorGradingLuts.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BlendColorGradingLuts.shader @@ -1,5 +1,5 @@ { - "Source": "BlendColorGradingLuts", + "Source": "BlendColorGradingLuts.azsl", "DrawList" : "forward", diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BloomBlurCS.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BloomBlurCS.shader index 467ccbf867..caaf411d29 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BloomBlurCS.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BloomBlurCS.shader @@ -1,5 +1,5 @@ { - "Source": "BloomBlurCS", + "Source": "BloomBlurCS.azsl", "DrawList" : "forward", diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BloomCompositeCS.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BloomCompositeCS.shader index 0be9455da1..26ada282ac 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BloomCompositeCS.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BloomCompositeCS.shader @@ -1,5 +1,5 @@ { - "Source": "BloomCompositeCS", + "Source": "BloomCompositeCS.azsl", "DrawList" : "forward", diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BloomDownsampleCS.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BloomDownsampleCS.shader index 7f33a041db..f78c4d4837 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BloomDownsampleCS.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/BloomDownsampleCS.shader @@ -1,5 +1,5 @@ { - "Source": "BloomDownsampleCS", + "Source": "BloomDownsampleCS.azsl", "DrawList" : "forward", diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ContrastAdaptiveSharpening.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ContrastAdaptiveSharpening.shader index 756ce0ec7a..75407d1095 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ContrastAdaptiveSharpening.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ContrastAdaptiveSharpening.shader @@ -1,5 +1,5 @@ { - "Source": "ContrastAdaptiveSharpening", + "Source": "ContrastAdaptiveSharpening.azsl", "ProgramSettings": { "EntryPoints": [ { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ConvertToAcescg.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ConvertToAcescg.shader index 82a70913d6..f5dae97b16 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ConvertToAcescg.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ConvertToAcescg.shader @@ -1,5 +1,5 @@ { - "Source" : "ConvertToAcescg", + "Source" : "ConvertToAcescg.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthDownsample.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthDownsample.shader index 77ae2b625c..6a66ff875d 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthDownsample.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthDownsample.shader @@ -1,5 +1,5 @@ { - "Source": "DepthDownsample", + "Source": "DepthDownsample.azsl", "ProgramSettings" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfField.azsli b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfField.azsli index bc94b4a793..f4c396253f 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfField.azsli +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfField.azsli @@ -14,6 +14,14 @@ inline float InvertDepth(float depth) return 1.0f - depth; } +inline float4 InvertDepth(float4 depth) +{ + // Convert depth from [1.0 - 0.0] to [0.0 - 1.0]. + // Set the front(near side) to 0.0 and the back(far side) to 1.0. + + return float4(1, 1, 1, 1) - depth; +} + inline float ConvertDofFactor(float depth, float far, float near, float focusDistance) { // dofFactor : The value Calculated from depth. diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldBlurBokeh.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldBlurBokeh.shader index 56dc124196..611f0a917c 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldBlurBokeh.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldBlurBokeh.shader @@ -1,5 +1,5 @@ { - "Source" : "DepthOfFieldBlurBokeh", + "Source" : "DepthOfFieldBlurBokeh.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldComposite.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldComposite.shader index a3d5890aa0..581b20d1a6 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldComposite.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldComposite.shader @@ -1,5 +1,5 @@ { - "Source" : "DepthOfFieldComposite", + "Source" : "DepthOfFieldComposite.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldDownSample.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldDownSample.shader index 87b0ffc56c..4093ace95b 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldDownSample.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldDownSample.shader @@ -1,5 +1,5 @@ { - "Source" : "DepthOfFieldDownSample", + "Source" : "DepthOfFieldDownSample.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldMask.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldMask.shader index 83c24caa4b..7535e57b5d 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldMask.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldMask.shader @@ -1,5 +1,5 @@ { - "Source" : "DepthOfFieldMask", + "Source" : "DepthOfFieldMask.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldPrepare.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldPrepare.shader index 9d7788a6e2..ec33cbf995 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldPrepare.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldPrepare.shader @@ -1,5 +1,5 @@ { - "Source" : "DepthOfFieldPrepare", + "Source" : "DepthOfFieldPrepare.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldWriteFocusDepthFromGpu.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldWriteFocusDepthFromGpu.shader index de6790c675..92243acb87 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldWriteFocusDepthFromGpu.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthOfFieldWriteFocusDepthFromGpu.shader @@ -1,5 +1,5 @@ { - "Source": "DepthOfFieldWriteFocusDepthFromGpu", + "Source": "DepthOfFieldWriteFocusDepthFromGpu.azsl", "ProgramSettings" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthToLinearDepth.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthToLinearDepth.shader index c8f2e6b48a..92e807fea6 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthToLinearDepth.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthToLinearDepth.shader @@ -1,5 +1,5 @@ { - "Source" : "DepthToLinearDepth", + "Source" : "DepthToLinearDepth.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthUpsample.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthUpsample.shader index 8ecf28f8d3..80a4fabb3c 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthUpsample.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DepthUpsample.shader @@ -1,5 +1,5 @@ { - "Source": "DepthUpsample", + "Source": "DepthUpsample.azsl", "ProgramSettings" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DiffuseSpecularMerge.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DiffuseSpecularMerge.shader index 87ee6fcb17..fea445335b 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DiffuseSpecularMerge.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DiffuseSpecularMerge.shader @@ -1,5 +1,5 @@ { - "Source" : "DiffuseSpecularMerge", + "Source" : "DiffuseSpecularMerge.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DisplayMapper.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DisplayMapper.shader index f9fd37efb7..2b5c9998ef 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DisplayMapper.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DisplayMapper.shader @@ -1,5 +1,5 @@ { - "Source" : "DisplayMapper", + "Source" : "DisplayMapper.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DisplayMapperOnlyGammaCorrection.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DisplayMapperOnlyGammaCorrection.shader index fdd4b90ef9..51d3b22d12 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DisplayMapperOnlyGammaCorrection.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DisplayMapperOnlyGammaCorrection.shader @@ -1,5 +1,5 @@ { - "Source" : "DisplayMapperOnlyGammaCorrection", + "Source" : "DisplayMapperOnlyGammaCorrection.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DownsampleLuminanceMinAvgMaxCS.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DownsampleLuminanceMinAvgMaxCS.shader index 3b541fe132..ba764ae2f1 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DownsampleLuminanceMinAvgMaxCS.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DownsampleLuminanceMinAvgMaxCS.shader @@ -1,5 +1,5 @@ { - "Source": "DownsampleLuminanceMinAvgMaxCS", + "Source": "DownsampleLuminanceMinAvgMaxCS.azsl", "ProgramSettings": { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DownsampleMinAvgMaxCS.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DownsampleMinAvgMaxCS.shader index a3eb7e6bdb..c7f560c17b 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DownsampleMinAvgMaxCS.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/DownsampleMinAvgMaxCS.shader @@ -1,5 +1,5 @@ { - "Source": "DownsampleMinAvgMaxCS", + "Source": "DownsampleMinAvgMaxCS.azsl", "ProgramSettings": { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/EyeAdaptation.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/EyeAdaptation.shader index e6e81b88fe..596af32317 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/EyeAdaptation.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/EyeAdaptation.shader @@ -1,5 +1,5 @@ { - "Source": "EyeAdaptation", + "Source": "EyeAdaptation.azsl", "ProgramSettings" : diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/FastDepthAwareBlurHor.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/FastDepthAwareBlurHor.shader index 74e5eeea42..d649584c44 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/FastDepthAwareBlurHor.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/FastDepthAwareBlurHor.shader @@ -1,5 +1,5 @@ { - "Source": "FastDepthAwareBlurHor", + "Source": "FastDepthAwareBlurHor.azsl", "ProgramSettings" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/FastDepthAwareBlurVer.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/FastDepthAwareBlurVer.shader index eb7b4225bb..b78cd6830a 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/FastDepthAwareBlurVer.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/FastDepthAwareBlurVer.shader @@ -1,5 +1,5 @@ { - "Source": "FastDepthAwareBlurVer", + "Source": "FastDepthAwareBlurVer.azsl", "ProgramSettings" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/FullscreenCopy.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/FullscreenCopy.shader index b6626cfc87..c82383167b 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/FullscreenCopy.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/FullscreenCopy.shader @@ -1,5 +1,5 @@ { - "Source" : "FullscreenCopy", + "Source" : "FullscreenCopy.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/HDRColorGrading.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/HDRColorGrading.azsl index bc33a7e920..3167b214dc 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/HDRColorGrading.azsl +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/HDRColorGrading.azsl @@ -40,6 +40,7 @@ ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback float m_whiteBalanceWeight; float m_whiteBalanceKelvin; float m_whiteBalanceTint; + float m_whiteBalanceLuminancePreservation; float m_splitToneBalance; float m_splitToneWeight; diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/HDRColorGrading.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/HDRColorGrading.shader index f76f6708b7..8b519b70e7 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/HDRColorGrading.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/HDRColorGrading.shader @@ -1,5 +1,5 @@ { - "Source" : "HDRColorGrading", + "Source" : "HDRColorGrading.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LookModificationTransform.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LookModificationTransform.shader index bc9dbda122..5b1d53da89 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LookModificationTransform.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LookModificationTransform.shader @@ -1,5 +1,5 @@ { - "Source" : "LookModificationTransform", + "Source" : "LookModificationTransform.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHeatmap.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHeatmap.shader index 9be5f49cc1..06368ff039 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHeatmap.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHeatmap.shader @@ -1,5 +1,5 @@ { - "Source" : "LuminanceHeatmap", + "Source" : "LuminanceHeatmap.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHistogramGenerator.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHistogramGenerator.shader index f3dd11e11a..23e57b164f 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHistogramGenerator.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHistogramGenerator.shader @@ -1,5 +1,5 @@ { - "Source": "LuminanceHistogramGenerator", + "Source": "LuminanceHistogramGenerator.azsl", "DrawList" : "forward", diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/MSAAResolveCustom.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/MSAAResolveCustom.shader index e23dba6d73..6a08dbaaed 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/MSAAResolveCustom.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/MSAAResolveCustom.shader @@ -1,5 +1,5 @@ { - "Source": "MSAAResolveCustom", + "Source": "MSAAResolveCustom.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/MSAAResolveDepth.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/MSAAResolveDepth.shader index c9439d1033..74011db7fa 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/MSAAResolveDepth.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/MSAAResolveDepth.shader @@ -1,5 +1,5 @@ { - "Source" : "MSAAResolveDepth", + "Source" : "MSAAResolveDepth.azsl", "DepthStencilState" : { "Depth" : { "Enable" : true, "CompareFunc" : "Always" } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ModulateTexture.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ModulateTexture.shader index d4cd98a5cd..4341d04390 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ModulateTexture.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ModulateTexture.shader @@ -1,5 +1,5 @@ { - "Source": "ModulateTexture", + "Source": "ModulateTexture.azsl", "ProgramSettings" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldCommon.azsli b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldCommon.azsli new file mode 100644 index 0000000000..cd0f334f29 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldCommon.azsli @@ -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 + * + */ + + +#include + +#define COC_EPSILON 0.0001 + +#define SAMPLES_LOOP_1 8 +#define SAMPLES_LOOP_2 16 +#define SAMPLES_LOOP_3 24 + +#define SAMPLES_LOOP_TOTAL 48 + +// Must match the struct in NewDepthOfFieldPasses.cpp +struct NewDepthOfFieldConstants +{ + float4 samplePositions[60]; // XY are sample positions (normalized so max lenght is 1) + // Z is the length of XY (0 - 1) + // W is unused +}; + diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldComposite.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldComposite.azsl new file mode 100644 index 0000000000..468f93db78 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldComposite.azsl @@ -0,0 +1,120 @@ +/* + * 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 "NewDepthOfFieldCommon.azsli" +#include "DepthOfField.azsli" + +#include + +#define COC_EPSILON 0.0001 + +ShaderResourceGroup PassSrg : SRG_PerPass +{ + Texture2D m_depth; + Texture2D m_halfResColorAndCoc; + + // Texture dimensions. XY channels are width and height and ZW channels are 1 / width and 1 / height + // Auto-filled by the pass system when "ShaderImageDimensionsConstant" is specified in the .pass file + float4 m_fullResDimensions; + float4 m_halfResDimensions; + + Sampler LinearSampler + { + MinFilter = Linear; + MagFilter = Linear; + MipFilter = Linear; + AddressU = Clamp; + AddressV = Clamp; + AddressW = Clamp; + }; +} + +PSOutput MainPS(VSOutput IN) +{ + // Sampling positions + float2 fullResPixelPos = IN.m_position.xy; + float2 halfResPixelPos = fullResPixelPos * 0.5f; + float2 fullResUV = fullResPixelPos * PassSrg::m_fullResDimensions.zw; + float2 halfResUV = halfResPixelPos * PassSrg::m_halfResDimensions.zw; + + // Full res CoC (Circle of Confusion) + float depth = PassSrg::m_depth.Sample(PassSrg::LinearSampler, fullResUV); + float far = ViewSrg::m_dof.m_cameraParameters.x; + float near = ViewSrg::m_dof.m_cameraParameters.y; + float focusDistance = ViewSrg::m_dof.m_cameraParameters.z; + float coc = ConvertDofFactor(InvertDepth(depth), far, near, focusDistance); + + // --- Weights based on CoC similarity --- + + // Gather CoCs + float4 cocGather = PassSrg::m_halfResColorAndCoc.GatherAlpha(PassSrg::LinearSampler, halfResUV); + + // Calculate differences + float4 diff = saturate(cocGather - coc); + + // Slide differences such that small difference (i.e. most similar CoC) will become 0 + // (which then gets inverted in the next step) + float minDiff = min4(diff); + diff -= minDiff; + + // Invert the differences with a slope multiplier of 2 + float4 cocDiffWeights = saturate(1 - (2 * diff)); + + // --- Weights based on pixel proximity --- + + // Based on which pixel we're shading, we'll be closer/farther to half res pixels + // Here are the pre-calculated weights, arranged to match the Gather pattern + // + // W Z + // X Y + // + // Note: These weights come down to the same contributions as if we did a linear sample + int2 pixel = int2(fullResPixelPos); + float4 weights = (pixel.x & 1) + ? ( (pixel.y & 1) ? float4(0.1875f, 0.0625f, 0.1875f, 0.5625f) + : float4(0.5625f, 0.1875f, 0.0625f, 0.1875f) ) + : ( (pixel.y & 1) ? float4(0.0625f, 0.1875f, 0.5625f, 0.1875f) + : float4(0.1875f, 0.5625f, 0.1875f, 0.0625f) ); + + // Combine and normalize weights + weights *= cocDiffWeights; + weights /= (weights.x + weights.y + weights.z + weights.w); + + // --- Color --- + + // For each color channel, do a gather and multiply the samples by the weights calculated above + float3 color; + float4 red = PassSrg::m_halfResColorAndCoc.GatherRed(PassSrg::LinearSampler, halfResUV); + color.r = dot(red, weights); + float4 blue = PassSrg::m_halfResColorAndCoc.GatherBlue(PassSrg::LinearSampler, halfResUV); + color.b = dot(blue, weights); + float4 green = PassSrg::m_halfResColorAndCoc.GatherGreen(PassSrg::LinearSampler, halfResUV); + color.g = dot(green, weights); + + + // --- Alpha --- + + // Calculate alpha such that we fully take the half res texture value if the CoC of the full + // resolution pixel is greater than the size of a half resolution pixel + float cocRadius = abs(coc) * ViewSrg::m_dof.m_cocToScreenRatio * 0.5f; + float alpha = saturate(cocRadius / PassSrg::m_halfResDimensions.w); + + // We may have objects in focus (CoC = 0) but that receive contribution from background bokeh + // (which have a negative CoC that we calculated in the large filter). Take the max here. + float minCoc = min4(cocGather); + alpha = max(alpha, -minCoc); + + PSOutput OUT; + OUT.m_color.rgb = color.rgb; + OUT.m_color.a = alpha; + return OUT; +} + diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldComposite.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldComposite.shader new file mode 100644 index 0000000000..42130d9ba8 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldComposite.shader @@ -0,0 +1,31 @@ +{ + "Source" : "NewDepthOfFieldComposite.azsl", + + "DepthStencilState" : + { + "Depth" : { "Enable" : false }, + "Stencil" : { "Enable" : false } + }, + + "BlendState" : { + "Enable" : true, + "BlendSource" : "AlphaSource", + "BlendDest" : "AlphaSourceInverse", + "BlendOp" : "Add" + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "MainVS", + "type": "Vertex" + }, + { + "name": "MainPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldDownsample.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldDownsample.azsl new file mode 100644 index 0000000000..01ff4b9493 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldDownsample.azsl @@ -0,0 +1,101 @@ +/* + * 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 "NewDepthOfFieldCommon.azsli" +#include "DepthOfField.azsli" + +#include + +#define COC_EPSILON 0.0001 + +ShaderResourceGroup PassSrg : SRG_PerPass +{ + Texture2D m_colorTexture; + Texture2D m_depth; + + // Texture dimensions. XY channels are width and height and ZW channels are 1 / width and 1 / height + // Auto-filled by the pass system when "ShaderImageDimensionsConstant" is specified in the .pass file + float4 m_inputDimensions; + float4 m_outputDimensions; + + Sampler PointSampler + { + MinFilter = Point; + MagFilter = Point; + MipFilter = Point; + AddressU = Clamp; + AddressV = Clamp; + AddressW = Clamp; + }; +} + +PSOutput MainPS(VSOutput IN) +{ + // Sampling positions + float2 outputPixelPos = IN.m_position.xy; + float2 inputPixelPos = outputPixelPos * 2.0f; + float2 inputUV = inputPixelPos * PassSrg::m_inputDimensions.zw; + + // Gather Depth + float4 depthGather = PassSrg::m_depth.Gather(PassSrg::PointSampler, inputUV); + depthGather = InvertDepth(depthGather); + + // Calculate CoC (Circle of Confusion) + float far = ViewSrg::m_dof.m_cameraParameters.x; + float near = ViewSrg::m_dof.m_cameraParameters.y; + float focusDistance = ViewSrg::m_dof.m_cameraParameters.z; + + float4 cocGather; + cocGather.x = ConvertDofFactor(depthGather.x, far, near, focusDistance); + cocGather.y = ConvertDofFactor(depthGather.y, far, near, focusDistance); + cocGather.z = ConvertDofFactor(depthGather.z, far, near, focusDistance); + cocGather.w = ConvertDofFactor(depthGather.w, far, near, focusDistance); + + // Clamp CoC + cocGather = clamp(cocGather, -1.0f, 1.0f); + + // Weight samples by CoC to avoid in focus pixels bleeding into bokeh effect + float4 weights = abs(cocGather) + COC_EPSILON; + weights = weights / (weights.x + weights.y + weights.z + weights.w); + + PSOutput OUT; + + // Red + float4 redGather = PassSrg::m_colorTexture.GatherRed(PassSrg::PointSampler, inputUV); + OUT.m_color.r = dot(redGather, weights); + + // Green + float4 greenGather = PassSrg::m_colorTexture.GatherGreen(PassSrg::PointSampler, inputUV); + OUT.m_color.g = dot(greenGather, weights); + + // Blue + float4 blueGather = PassSrg::m_colorTexture.GatherBlue(PassSrg::PointSampler, inputUV); + OUT.m_color.b = dot(blueGather, weights); + + // CoC - Take the CoC with the maximum absolute value (note CoC can be negative) to get the fullest bokeh effect + // The above weighting by CoC mitigates bokeh bleeding from taking the max CoC + float coc = cocGather.x; + coc = abs(coc) < abs(cocGather.y) ? cocGather.y : coc; + coc = abs(coc) < abs(cocGather.z) ? cocGather.z : coc; + coc = abs(coc) < abs(cocGather.w) ? cocGather.w : coc; + + // CoC weighting #2: we use linear sampling when we compute the CoC blur + // This can lead to in focus pixels bleeding into the bokeh blur + // Pre-multiply the color values by the CoC to avoid this type of bleeding + // Because we then re-multiply by the CoC value after the linear sampling, we want to avoid coc values of 0 + // See http://advances.realtimerendering.com/s2013/Sousa_Graphics_Gems_CryENGINE3.pptx + coc = abs(coc) > COC_EPSILON ? coc : -COC_EPSILON; + OUT.m_color.rgb *= abs(coc); + OUT.m_color.a = coc; + + + return OUT; +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldDownsample.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldDownsample.shader new file mode 100644 index 0000000000..8a9fe62fd7 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldDownsample.shader @@ -0,0 +1,22 @@ +{ + "Source" : "NewDepthOfFieldDownsample.azsl", + + "DepthStencilState" : { + "Depth" : { "Enable" : false } + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "MainVS", + "type": "Vertex" + }, + { + "name": "MainPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldFilterLarge.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldFilterLarge.azsl new file mode 100644 index 0000000000..3b979b9c39 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldFilterLarge.azsl @@ -0,0 +1,170 @@ +/* + * 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 "DepthOfField.azsli" +#include "NewDepthOfFieldCommon.azsli" + +#include + +ShaderResourceGroup PassSrg : SRG_PerPass +{ + Texture2D m_colorAndCoc; + Texture2D m_minMaxCocTile; + + // Texture dimensions. XY channels are width and height and ZW channels are 1 / width and 1 / height + // Auto-filled by the pass system when "ShaderImageDimensionsConstant" is specified in the .pass file + float4 m_textureDimensions; + + NewDepthOfFieldConstants m_dofConstants; + + Sampler LinearSampler + { + MinFilter = Linear; + MagFilter = Linear; + MipFilter = Linear; + AddressU = Clamp; + AddressV = Clamp; + AddressW = Clamp; + }; + + Sampler PointSampler + { + MinFilter = Point; + MagFilter = Point; + MipFilter = Point; + AddressU = Clamp; + AddressV = Clamp; + AddressW = Clamp; + }; +} + +float3 GetOffset(uint index, float2 offsetUVMultiplier) +{ + float3 offset = PassSrg::m_dofConstants.samplePositions[index].xyz; + offset.xy *= offsetUVMultiplier; + return offset; +} + +float CalculateWeight(float offsetRadius, float samplingRadius, float sampleCoc, float centerCoc) +{ + // The maximum distance for which samples are valid is the min of the sample CoC and the center CoC + float maxRadius = abs(min(sampleCoc, centerCoc)); + + // Easy human readable calculations: + // radius = samplingRadius * offsetRadius; + // falloff = maxRadius - radius; + // weight = 1 + (4 * falloff) + // + // The same thing in mad form: + float falloff = mad(-samplingRadius, offsetRadius, maxRadius); + return saturate(mad(4, falloff, 1)); +} + +// This shader blurs by sampling 48 pixels in a circle around the center pixel +// See http://advances.realtimerendering.com/s2013/Sousa_Graphics_Gems_CryENGINE3.pptx +// for a detailed explanation. +PSOutput MainPS(VSOutput IN) +{ + // Get center sample + float2 pixelUV = IN.m_texCoord; + float4 color = PassSrg::m_colorAndCoc.Sample(PassSrg::LinearSampler, pixelUV).rgba; + float centerCoc = color.a; + + // Get tile min CoC + int2 tile = int2(IN.m_position.xy) / 16; + float minCoc = PassSrg::m_minMaxCocTile[tile].x; + + // Aspect ratio is needed because sample offsets are calculated in a perfect circle, but + // UV space is stretched due to normalized device coordinates. Correct this with aspect ratio + // Aspect ratio = texture.x / texture.y = dimensions.x * dimensions.w + float aspectRatio = PassSrg::m_textureDimensions.x * PassSrg::m_textureDimensions.w; + + // Sampling radius + float cocRadius = max( abs(centerCoc), -minCoc); + float screenRadius = cocRadius * ViewSrg::m_dof.m_cocToScreenRatio * 0.5f; + float2 offsetMultiplier = float2(screenRadius / aspectRatio, screenRadius); + + // Background samples are samples behind the current pixel. Because of how depth of field works, + // these pixels can contribute to the center pixel even if they are out of range of the center pixel's CoC + // We accumulate them seperately and calculate a new estimated alpha value based on the ratio of samples + // that were background pixels. + float4 backgroundColor = float4(0, 0, 0, 0); + + // If there are only positive CoCs in our region, we don't need to consider background blur + // Do the faster and nicer approach + if(minCoc >= 0) + { + for(uint i = 0; i < SAMPLES_LOOP_TOTAL; ++i) + { + // Calculate sample offset + float3 offset = GetOffset(i, offsetMultiplier); + + // Get sample + float4 sampleColorCoc = PassSrg::m_colorAndCoc.Sample(PassSrg::LinearSampler, pixelUV + offset.xy).rgba; + + // Calculate weight for sample + float weight = CalculateWeight(offset.z, cocRadius, sampleColorCoc.a, centerCoc); + + // Accumulate + color += weight * sampleColorCoc; + } + } + else // Some CoCs in the region are negative, need to consider possible background bokeh contribution + { + // Distance behind which samples are considered background samples + float backgroundMin = min(0, centerCoc); + + // Linear sampling colors pre-multiplied with CoC yields artefacts when combined with this background technique + // We therefore do point sampling and unpack the original color value per sample + color.rgb /= abs(color.a); + color.a = 1; + + for(uint i = 0; i < SAMPLES_LOOP_TOTAL; ++i) + { + // Calculate sample offset + float3 offset = GetOffset(i, offsetMultiplier); + + // Get sample + unpack + float4 sampleColorCoc = PassSrg::m_colorAndCoc.Sample(PassSrg::PointSampler, pixelUV + offset.xy).rgba; + sampleColorCoc.rgb /= abs(sampleColorCoc.a); + + // Calculate weight for sample + float weight = CalculateWeight(offset.z, cocRadius, sampleColorCoc.a, centerCoc); + sampleColorCoc.rgb *= weight; + + bool isBackground = (sampleColorCoc.a < backgroundMin); + + // We accumulate weight in alpha channel of color and backgroundColor + sampleColorCoc.a = weight; + + // Accumulate + backgroundColor += isBackground * sampleColorCoc; + color += !isBackground * sampleColorCoc; + } + + // Average background samples + backgroundColor.rgb /= max(backgroundColor.a, COC_EPSILON); + } + + // Calculate background ratio. If greater than the current CoC, replace the current CoC with + // background ratio. This is so background bokeh effects will still render on in-focus objects + float backgroundRatio = saturate( backgroundColor.a / float(SAMPLES_LOOP_TOTAL) ); + float alpha = backgroundRatio > abs(centerCoc) ? -backgroundRatio : centerCoc; + + // Average accumulated color samples and combine with background samples + color.rgb /= max(color.a, COC_EPSILON); + color = lerp(color, backgroundColor, backgroundRatio); + + PSOutput OUT = (PSOutput)0; + OUT.m_color.rgb = color.rgb; + OUT.m_color.a = alpha; + return OUT; +} + diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldFilterLarge.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldFilterLarge.shader new file mode 100644 index 0000000000..f11ac01e5c --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldFilterLarge.shader @@ -0,0 +1,22 @@ +{ + "Source" : "NewDepthOfFieldFilterLarge.azsl", + + "DepthStencilState" : { + "Depth" : { "Enable" : false } + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "MainVS", + "type": "Vertex" + }, + { + "name": "MainPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldFilterSmall.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldFilterSmall.azsl new file mode 100644 index 0000000000..38762254a8 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldFilterSmall.azsl @@ -0,0 +1,121 @@ +/* + * 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 "DepthOfField.azsli" +#include "NewDepthOfFieldCommon.azsli" + +#include + +ShaderResourceGroup PassSrg : SRG_PerPass +{ + Texture2D m_colorAndCoc; + Texture2D m_minMaxCocTile; + + // Texture dimensions. XY channels are width and height and ZW channels are 1 / width and 1 / height + // Auto-filled by the pass system when "ShaderImageDimensionsConstant" is specified in the .pass file + float4 m_textureDimensions; + + NewDepthOfFieldConstants m_dofConstants; + + Sampler LinearSampler + { + MinFilter = Linear; + MagFilter = Linear; + MipFilter = Linear; + AddressU = Clamp; + AddressV = Clamp; + AddressW = Clamp; + }; + + Sampler PointSampler + { + MinFilter = Point; + MagFilter = Point; + MipFilter = Point; + AddressU = Clamp; + AddressV = Clamp; + AddressW = Clamp; + }; +} + +float3 GetOffset(uint index, float2 offsetUVMultiplier) +{ + float3 offset = PassSrg::m_dofConstants.samplePositions[index].xyz; + offset.xy *= offsetUVMultiplier; + return offset; +} + +float CalculateWeight(float offsetRadius, float samplingRadius, float sampleCoc, float centerCoc) +{ + // The maximum distance for which samples are valid is the min of the sample CoC and the center CoC + float maxRadius = abs(min(sampleCoc, centerCoc)); + + // Easy human readable calculations: + // radius = samplingRadius * offsetRadius; + // falloff = maxRadius - radius; + // weight = 1 + (4 * falloff) + // + // The same thing in mad form: + float falloff = mad(-samplingRadius, offsetRadius, maxRadius); + return saturate(mad(4, falloff, 1)); +} + +// This shader attempts to fill the gaps left by the large filter by sampling 8 pixels around the center pixel +// See http://advances.realtimerendering.com/s2013/Sousa_Graphics_Gems_CryENGINE3.pptx +// for a detailed overview of the technique +PSOutput MainPS(VSOutput IN) +{ + // Get center sample + float2 pixelUV = IN.m_texCoord.xy; + float4 color = PassSrg::m_colorAndCoc.Sample(PassSrg::PointSampler, pixelUV).rgba; + float centerCoc = color.a; + + // Get tile min CoC + int2 tile = int2(IN.m_position.xy) / 16; + float minCoc = PassSrg::m_minMaxCocTile[tile].x; + + // Aspect ratio is needed because sample offsets are calculated in a perfect circle, but + // UV space is stretched due to normalized device coordinates. Correct this with aspect ratio + // Aspect ratio = texture.x / texture.y = dimensions.x * dimensions.w + float aspectRatio = PassSrg::m_textureDimensions.x * PassSrg::m_textureDimensions.w; + + // Sampling radius + float cocRadius = max( abs(centerCoc), -minCoc) * 0.5f; // Small filter pass so half the radius + float screenRadius = cocRadius * ViewSrg::m_dof.m_cocToScreenRatio * 0.5f; + float2 offsetMultiplier = float2(screenRadius / aspectRatio, screenRadius); + + // Weight accumulation. Start with 1 for center pixel. + float totalWeight = 1; + + for(uint i = 0; i < SAMPLES_LOOP_1; ++i) + { + // Calculate sample offset + float3 offset = GetOffset(i, offsetMultiplier); + + // Get sample + float4 sampleColorCoc = PassSrg::m_colorAndCoc.Sample(PassSrg::PointSampler, pixelUV + offset.xy).rgba; + + // Calculate weight + float weight = CalculateWeight(offset.z, cocRadius, sampleColorCoc.a, centerCoc); + + // Accumulate sample and weight + color.rgb += sampleColorCoc.rgb * weight; + totalWeight += weight; + } + + + // Normalize accumulated sample + color.rgb /= totalWeight; + + PSOutput OUT; + OUT.m_color.rgb = color.rgb; + OUT.m_color.a = centerCoc; + return OUT; +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldFilterSmall.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldFilterSmall.shader new file mode 100644 index 0000000000..17387a4390 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldFilterSmall.shader @@ -0,0 +1,22 @@ +{ + "Source" : "NewDepthOfFieldFilterSmall.azsl", + + "DepthStencilState" : { + "Depth" : { "Enable" : false } + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "MainVS", + "type": "Vertex" + }, + { + "name": "MainPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldTile3x3.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldTile3x3.azsl new file mode 100644 index 0000000000..6c56b0b79c --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldTile3x3.azsl @@ -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 + * + */ + +#include +#include +#include "NewDepthOfFieldCommon.azsli" + +ShaderResourceGroup PassSrg : SRG_PerPass +{ + Texture2D m_minMaxSource; + + // Texture dimensions. XY channels are width and height and ZW channels are 1 / width and 1 / height + // Auto-filled by the pass system when "ShaderImageDimensionsConstant" is specified in the .pass file + float4 m_textureDimensions; + + Sampler PointSampler + { + MinFilter = Point; + MagFilter = Point; + MipFilter = Point; + AddressU = Clamp; + AddressV = Clamp; + AddressW = Clamp; + }; +} + +struct PSOutput +{ + float2 m_color : SV_Target0; +}; + +// Expands the min and max tiles so each tile contains the min and max of it's 3x3 neighborhood +PSOutput MainPS(VSOutput IN) +{ + // We want the min/max in a 3x3 region. Start sampling up left. + float2 startPixelPos = IN.m_position.xy - float2(1, 1); + + float2 pixelSizeInUV = PassSrg::m_textureDimensions.zw; + float2 startUV = startPixelPos * pixelSizeInUV; + + float cocMin = 1.0f; + float cocMax = -1.0f; + + // Gather min/max in 3x3 region + [unroll] + for(float Y = 0.0f; Y < 3.0f; Y += 1.0f) + { + [unroll] + for(float X = 0.0f; X < 3.0f; X += 1.0f) + { + float2 sampleUV = mad(float2(X, Y), pixelSizeInUV, startUV); + float2 minMax = PassSrg::m_minMaxSource.SampleLevel(PassSrg::PointSampler, sampleUV, 0).xy; + + cocMin = min(cocMin, minMax.x); + cocMax = max(cocMax, minMax.y); + } + } + + PSOutput output; + output.m_color.x = cocMin; + output.m_color.y = cocMax; + return output; +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldTile3x3.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldTile3x3.shader new file mode 100644 index 0000000000..6ee990c5a7 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldTile3x3.shader @@ -0,0 +1,22 @@ +{ + "Source" : "NewDepthOfFieldTile3x3.azsl", + + "DepthStencilState" : { + "Depth" : { "Enable" : false } + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "MainVS", + "type": "Vertex" + }, + { + "name": "MainPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldTileReduce.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldTileReduce.azsl new file mode 100644 index 0000000000..6d55648f22 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldTileReduce.azsl @@ -0,0 +1,87 @@ +/* + * 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 "NewDepthOfFieldCommon.azsli" + +ShaderResourceGroup PassSrg : SRG_PerPass +{ + // For this shader we're only interested in the CoC values, which are in the alpha channel + Texture2D m_colorAndCoC; + + // Tiled min/max CoC values + RWTexture2D m_minMaxCoC; + + // Texture dimensions. XY channels are width and height and ZW channels are 1 / width and 1 / height + // Auto-filled by the pass system when "ShaderImageDimensionsConstant" is specified in the .pass file + float4 m_inputDimensions; + float4 m_outputDimensions; + + Sampler LinearSampler + { + MinFilter = Linear; + MagFilter = Linear; + MipFilter = Linear; + AddressU = Clamp; + AddressV = Clamp; + AddressW = Clamp; + }; +} + +groupshared uint LDS_MIN_COC[8]; +groupshared uint LDS_MAX_COC[8]; + +// Calculates the min and max CoC (Circle of Confusion) for 16x16 pixel tiles +[numthreads(8, 8, 1)] +void MainCS(uint3 group_thread_id : SV_GroupThreadID, uint3 group_id : SV_GroupID, uint3 dispatch_id: SV_DispatchThreadID, uint linear_id : SV_GroupIndex) +{ + // Initialize groupshared mem for atomic min/max operations + if(group_thread_id.y == 0) + { + LDS_MIN_COC[group_thread_id.x] = 0xFFFFFFFF; + LDS_MAX_COC[group_thread_id.x] = 0; + } + + // We use gather to get 2x2 values at once, so thread samples are spaced 2 pixels apart (+1 so the sample position is in between the four pixels) + float2 samplePos = float2(dispatch_id.xy) * 2 + float2(1, 1); + float2 sampleUV = samplePos * PassSrg::m_inputDimensions.zw; + + // Gather CoC + float4 cocGather = PassSrg::m_colorAndCoC.GatherAlpha(PassSrg::LinearSampler, sampleUV); + float cocMin = min4(cocGather); + float cocMax = max4(cocGather); + + // For atomic min/max to work with uints, floating point values should be positive + // Map from [-1, 1] range to [0, 2] and cast as uint + InterlockedMin( LDS_MIN_COC[group_thread_id.x], asuint(cocMin + 1) ); + InterlockedMax( LDS_MAX_COC[group_thread_id.x], asuint(cocMax + 1) ); + + // Sync LDS + GroupMemoryBarrierWithGroupSync(); + + if(group_thread_id.y != 0) + { + return; + } + + // Min the mins and max the maxs + InterlockedMin( LDS_MIN_COC[0], LDS_MIN_COC[group_thread_id.x] ); + InterlockedMax( LDS_MAX_COC[0], LDS_MAX_COC[group_thread_id.x] ); + + // Each group write to just one pixel. If we're the last thread in the group, write out + if(group_thread_id.x == 0) + { + // Unpack uints + cocMin = asfloat(LDS_MIN_COC[0]) - 1; + cocMax = asfloat(LDS_MAX_COC[0]) - 1; + + // Output min/max coc of 16x16 region + PassSrg::m_minMaxCoC[group_id.xy] = float2(cocMin, cocMax); + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldTileReduce.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldTileReduce.shader new file mode 100644 index 0000000000..06313cdbf2 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/NewDepthOfFieldTileReduce.shader @@ -0,0 +1,14 @@ +{ + "Source" : "NewDepthOfFieldTileReduce.azsl", + + "ProgramSettings" : + { + "EntryPoints": + [ + { + "name" : "MainCS", + "type" : "Compute" + } + ] + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/OutputTransform.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/OutputTransform.shader index 1a13a21063..5cb652e93d 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/OutputTransform.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/OutputTransform.shader @@ -1,5 +1,5 @@ { - "Source" : "OutputTransform", + "Source" : "OutputTransform.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAABlendingWeightCalculation.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAABlendingWeightCalculation.shader index 119f10fa47..cbca022dca 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAABlendingWeightCalculation.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAABlendingWeightCalculation.shader @@ -1,5 +1,5 @@ { - "Source" : "SMAABlendingWeightCalculation", + "Source" : "SMAABlendingWeightCalculation.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAAConvertToPerceptualColor.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAAConvertToPerceptualColor.shader index c009c62255..cccccd7418 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAAConvertToPerceptualColor.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAAConvertToPerceptualColor.shader @@ -1,5 +1,5 @@ { - "Source" : "SMAAConvertToPerceptualColor", + "Source" : "SMAAConvertToPerceptualColor.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAAEdgeDetection.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAAEdgeDetection.shader index 43110a1944..cb421a7e63 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAAEdgeDetection.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAAEdgeDetection.shader @@ -1,5 +1,5 @@ { - "Source" : "SMAAEdgeDetection", + "Source" : "SMAAEdgeDetection.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAANeighborhoodBlending.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAANeighborhoodBlending.shader index b93672b961..caeffcff15 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAANeighborhoodBlending.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SMAANeighborhoodBlending.shader @@ -1,5 +1,5 @@ { - "Source" : "SMAANeighborhoodBlending", + "Source" : "SMAANeighborhoodBlending.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ScreenSpaceSubsurfaceScatteringCS.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ScreenSpaceSubsurfaceScatteringCS.shader index a54060f093..f6e89db84f 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ScreenSpaceSubsurfaceScatteringCS.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/ScreenSpaceSubsurfaceScatteringCS.shader @@ -1,5 +1,5 @@ { - "Source": "ScreenSpaceSubsurfaceScatteringCS", + "Source": "ScreenSpaceSubsurfaceScatteringCS.azsl", "ProgramSettings": { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SsaoCompute.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SsaoCompute.shader index 777400ca97..226f995cb6 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SsaoCompute.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/SsaoCompute.shader @@ -1,5 +1,5 @@ { - "Source": "SsaoCompute", + "Source": "SsaoCompute.azsl", "ProgramSettings" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/Taa.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/Taa.shader index f30ff92f20..fb0d510a8a 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/Taa.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/Taa.shader @@ -1,5 +1,5 @@ { - "Source": "Taa", + "Source": "Taa.azsl", "ProgramSettings": { "EntryPoints": [ { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/UniformColor.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/UniformColor.shader index 422199c420..5db8c97699 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/UniformColor.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/UniformColor.shader @@ -1,5 +1,5 @@ { - "Source" : "UniformColor", + "Source" : "UniformColor.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionComposite.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionComposite.shader index 847bf5e300..91bcd9b1b4 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionComposite.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionComposite.shader @@ -1,5 +1,5 @@ { - "Source" : "ReflectionComposite", + "Source" : "ReflectionComposite.azsl", "RasterState" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionGlobalFullscreen.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionGlobalFullscreen.shader index 445a29c74f..f0b962c2eb 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionGlobalFullscreen.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionGlobalFullscreen.shader @@ -1,5 +1,5 @@ { - "Source" : "ReflectionGlobalFullscreen", + "Source" : "ReflectionGlobalFullscreen.azsl", "RasterState" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeBlendWeight.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeBlendWeight.shader index 1da653f1e7..0e4cc83413 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeBlendWeight.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeBlendWeight.shader @@ -1,5 +1,5 @@ { - "Source" : "ReflectionProbeBlendWeight", + "Source" : "ReflectionProbeBlendWeight.azsl", "RasterState" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeRenderInner.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeRenderInner.shader index 76cab60b3c..20dd4bcb56 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeRenderInner.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeRenderInner.shader @@ -1,5 +1,5 @@ { - "Source" : "ReflectionProbeRenderInner", + "Source" : "ReflectionProbeRenderInner.azsl", "RasterState" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeRenderOuter.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeRenderOuter.shader index 9e60a5faf9..ce7b1c0487 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeRenderOuter.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeRenderOuter.shader @@ -1,5 +1,5 @@ { - "Source" : "ReflectionProbeRenderOuter", + "Source" : "ReflectionProbeRenderOuter.azsl", "RasterState" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeStencil.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeStencil.shader index 538d746fba..7c23487dc1 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeStencil.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeStencil.shader @@ -1,5 +1,5 @@ { - "Source" : "ReflectionProbeStencil", + "Source" : "ReflectionProbeStencil.azsl", "RasterState" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceBlurHorizontal.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceBlurHorizontal.shader index 013c42da57..aea21136dc 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceBlurHorizontal.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceBlurHorizontal.shader @@ -1,5 +1,5 @@ { - "Source" : "ReflectionScreenSpaceBlurHorizontal", + "Source" : "ReflectionScreenSpaceBlurHorizontal.azsl", "RasterState" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceBlurVertical.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceBlurVertical.shader index d5ed94c4a9..dcebb4d2ae 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceBlurVertical.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceBlurVertical.shader @@ -1,5 +1,5 @@ { - "Source" : "ReflectionScreenSpaceBlurVertical", + "Source" : "ReflectionScreenSpaceBlurVertical.azsl", "RasterState" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceComposite.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceComposite.shader index d04189b0ba..d23adf898d 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceComposite.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceComposite.shader @@ -1,5 +1,5 @@ { - "Source" : "ReflectionScreenSpaceComposite", + "Source" : "ReflectionScreenSpaceComposite.azsl", "RasterState" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceTrace.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceTrace.shader index a6e0b5df4e..563e0e3276 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceTrace.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceTrace.shader @@ -1,5 +1,5 @@ { - "Source" : "ReflectionScreenSpaceTrace", + "Source" : "ReflectionScreenSpaceTrace.azsl", "RasterState" : { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/DepthExponentiation.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/DepthExponentiation.shader index e2f860a169..7d969a491b 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/DepthExponentiation.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/DepthExponentiation.shader @@ -1,5 +1,5 @@ { - "Source" : "DepthExponentiation", + "Source" : "DepthExponentiation.azsl", "DrawList" : "shadow", diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/KawaseShadowBlur.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/KawaseShadowBlur.shader index dacacdafff..4f317b31e6 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/KawaseShadowBlur.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/KawaseShadowBlur.shader @@ -1,5 +1,5 @@ { - "Source" : "KawaseShadowBlur", + "Source" : "KawaseShadowBlur.azsl", "DrawList" : "shadow", diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/Shadowmap.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/Shadowmap.shader index 6ef5be872a..d56f40ccdb 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/Shadowmap.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/Shadowmap.shader @@ -1,5 +1,5 @@ { - "Source" : "Shadowmap", + "Source" : "Shadowmap.azsl", "DepthStencilState" : { "Depth" : { "Enable" : true, "CompareFunc" : "LessEqual" } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/ShadowmapSkin.shader b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/ShadowmapSkin.shader index 14c1352c08..40379d9193 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/ShadowmapSkin.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Shadow/ShadowmapSkin.shader @@ -1,5 +1,5 @@ { - "Source" : "ShadowmapSkin", + "Source" : "ShadowmapSkin.azsl", "DepthStencilState" : { "Depth" : { "Enable" : true, "CompareFunc" : "LessEqual" } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/SkinnedMesh/LinearSkinningCS.shader b/Gems/Atom/Feature/Common/Assets/Shaders/SkinnedMesh/LinearSkinningCS.shader index 6bb4f3c289..a853d61dd8 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/SkinnedMesh/LinearSkinningCS.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/SkinnedMesh/LinearSkinningCS.shader @@ -1,5 +1,5 @@ { - "Source": "LinearSkinningCS", + "Source": "LinearSkinningCS.azsl", "ProgramSettings": { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox.shader b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox.shader index 4edd81d07f..ff91e2f104 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox.shader @@ -1,5 +1,5 @@ { - "Source" : "SkyBox", + "Source" : "SkyBox.azsl", "DepthStencilState" : { "Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.shader b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.shader index ec80d4a20e..ecd4c5b18e 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.shader @@ -1,5 +1,5 @@ { - "Source" : "SkyBox_TwoOutputs", + "Source" : "SkyBox_TwoOutputs.azsl", "DepthStencilState" : { "Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" } diff --git a/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake b/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake index 123e5da7ba..94b711e86c 100644 --- a/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake +++ b/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake @@ -1,9 +1,8 @@ # -# 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 -# +# 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 @@ -161,8 +160,6 @@ set(FILES Passes/LutGeneration.pass Passes/MainPipeline.pass Passes/MainPipelineRenderToTexture.pass - Passes/ThumbnailPipeline.pass - Passes/ThumbnailPipelineRenderToTexture.pass Passes/MeshMotionVector.pass Passes/ModulateTexture.pass Passes/MorphTarget.pass @@ -170,6 +167,13 @@ set(FILES Passes/MSAAResolveColor.pass Passes/MSAAResolveCustom.pass Passes/MSAAResolveDepth.pass + Passes/NewDepthOfField.pass + Passes/NewDepthOfFieldComposite.pass + Passes/NewDepthOfFieldDownsample.pass + Passes/NewDepthOfFieldFilterLarge.pass + Passes/NewDepthOfFieldFilterSmall.pass + Passes/NewDepthOfFieldTile3x3.pass + Passes/NewDepthOfFieldTileReduce.pass Passes/OpaqueParent.pass Passes/PostProcessParent.pass Passes/ProjectedShadowmaps.pass @@ -205,11 +209,17 @@ set(FILES Passes/SsaoParent.pass Passes/SubsurfaceScattering.pass Passes/Taa.pass + Passes/ToolsPipeline.pass + Passes/ToolsPipelineRenderToTexture.pass Passes/Transparent.pass Passes/TransparentParent.pass Passes/UI.pass Passes/UIParent.pass + Scripts/material_find_overrides_demo.lua Scripts/material_property_overrides_demo.lua + ShaderLib/3rdParty/Features/PostProcessing/KelvinToRgb.azsli + ShaderLib/3rdParty/Features/PostProcessing/PSstyleColorBlends_NonSeparable.azsli + ShaderLib/3rdParty/Features/PostProcessing/PSstyleColorBlends_Separable.azsli ShaderLib/Atom/Features/BlendUtility.azsli ShaderLib/Atom/Features/IndirectRendering.azsli ShaderLib/Atom/Features/MatrixUtility.azsli @@ -218,6 +228,8 @@ set(FILES ShaderLib/Atom/Features/SphericalHarmonicsUtility.azsli ShaderLib/Atom/Features/SrgSemantics.azsli ShaderLib/Atom/Features/ColorManagement/TransformColor.azsli + ShaderLib/Atom/Features/ColorManagement/GeneratedTransforms/AcesCcToAcesCg.azsli + ShaderLib/Atom/Features/ColorManagement/GeneratedTransforms/AcesCgToAcesCc.azsli ShaderLib/Atom/Features/ColorManagement/GeneratedTransforms/AcesCg_To_LinearSrgb.azsli ShaderLib/Atom/Features/ColorManagement/GeneratedTransforms/Aces_To_AcesCg.azsli ShaderLib/Atom/Features/ColorManagement/GeneratedTransforms/CalculateLuminance_AcesCg.azsli @@ -225,8 +237,6 @@ set(FILES ShaderLib/Atom/Features/ColorManagement/GeneratedTransforms/LinearSrgb_To_AcesCg.azsli ShaderLib/Atom/Features/ColorManagement/GeneratedTransforms/LinearSrgb_To_Srgb.azsli ShaderLib/Atom/Features/ColorManagement/GeneratedTransforms/Srgb_To_LinearSrgb.azsli - ShaderLib/Atom/Features/ColorManagement/GeneratedTransforms/AcesCcToAcesCg.azsli - ShaderLib/Atom/Features/ColorManagement/GeneratedTransforms/AcesCgToAcesCc.azsli ShaderLib/Atom/Features/CoreLights/PhotometricValue.azsli ShaderLib/Atom/Features/Decals/DecalTextureUtil.azsli ShaderLib/Atom/Features/LightCulling/LightCullingShared.azsli @@ -284,17 +294,23 @@ set(FILES ShaderLib/Atom/Features/PostProcessing/GlyphRender.azsli ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl ShaderLib/Atom/Features/PostProcessing/PostProcessUtil.azsli + ShaderLib/Atom/Features/PostProcessing/Shapers.azsli + ShaderLib/Atom/Features/RayTracing/RayTracingMaterialSrg.azsli + ShaderLib/Atom/Features/RayTracing/RayTracingMaterialUtils.azsli ShaderLib/Atom/Features/RayTracing/RayTracingSceneSrg.azsli + ShaderLib/Atom/Features/RayTracing/RayTracingSceneUtils.azsli + ShaderLib/Atom/Features/RayTracing/RayTracingSrgs.azsl + ShaderLib/Atom/Features/RayTracing/RayTracingSrgs.shader ShaderLib/Atom/Features/ScreenSpace/ScreenSpaceUtil.azsli ShaderLib/Atom/Features/Shadow/BicubicPcfFilters.azsli ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli + ShaderLib/Atom/Features/Shadow/NormalOffsetShadows.azsli ShaderLib/Atom/Features/Shadow/ProjectedShadow.azsli + ShaderLib/Atom/Features/Shadow/ReceiverPlaneDepthBias.azsli ShaderLib/Atom/Features/Shadow/Shadow.azsli ShaderLib/Atom/Features/Shadow/ShadowmapAtlasLib.azsli + ShaderLib/Atom/Features/Skin/SkinObjectSrg.azsli ShaderLib/Atom/Features/Vertex/VertexHelper.azsli - ShaderLib/3rdParty/Features/PostProcessing/KelvinToRgb.azsli - ShaderLib/3rdParty/Features/PostProcessing/PSstyleColorBlends_NonSeparable.azsli - ShaderLib/3rdParty/Features/PostProcessing/PSstyleColorBlends_Separable.azsli ShaderResourceGroups/SceneSrg.azsli ShaderResourceGroups/SceneSrgAll.azsli ShaderResourceGroups/ViewSrg.azsli @@ -305,6 +321,8 @@ set(FILES ShaderResourceGroups/PostProcessing/SceneSrg.azsli ShaderResourceGroups/PostProcessing/ViewSrg.azsli ShaderResourceGroups/SkyBox/SceneSrg.azsli + Shaders/ForwardPassSrg.azsl + Shaders/ForwardPassSrg.shader Shaders/AuxGeom/AuxGeomObject.azsl Shaders/AuxGeom/AuxGeomObject.shader Shaders/AuxGeom/AuxGeomObjectLit.azsl @@ -321,14 +339,20 @@ set(FILES Shaders/ColorGrading/LutGeneration.shader Shaders/Depth/DepthPass.azsl Shaders/Depth/DepthPass.shader + Shaders/Depth/DepthPassCommon.azsli + Shaders/Depth/DepthPassSkin.azsl + Shaders/Depth/DepthPassSkin.shader Shaders/Depth/DepthPassTransparentMax.shader Shaders/Depth/DepthPassTransparentMin.shader Shaders/DiffuseGlobalIllumination/DiffuseComposite.azsl Shaders/DiffuseGlobalIllumination/DiffuseComposite.shader + Shaders/DiffuseGlobalIllumination/DiffuseComposite_nomsaa.azsl Shaders/DiffuseGlobalIllumination/DiffuseGlobalFullscreen.azsl Shaders/DiffuseGlobalIllumination/DiffuseGlobalFullscreen.shader + Shaders/DiffuseGlobalIllumination/DiffuseGlobalFullscreen_nomsaa.azsl Shaders/DiffuseGlobalIllumination/DiffuseProbeGridDownsample.azsl Shaders/DiffuseGlobalIllumination/DiffuseProbeGridDownsample.shader + Shaders/DiffuseGlobalIllumination/DiffuseProbeGridDownsample_nomsaa.azsl Shaders/ImGui/ImGui.azsl Shaders/ImGui/ImGui.shader Shaders/LightCulling/LightCulling.azsl @@ -348,6 +372,9 @@ set(FILES Shaders/MotionVector/CameraMotionVector.shader Shaders/MotionVector/MeshMotionVector.azsl Shaders/MotionVector/MeshMotionVector.shader + Shaders/MotionVector/MeshMotionVectorCommon.azsli + Shaders/MotionVector/MeshMotionVectorSkin.azsl + Shaders/MotionVector/MeshMotionVectorSkin.shader Shaders/PostProcessing/AcesOutputTransformLut.azsl Shaders/PostProcessing/AcesOutputTransformLut.shader Shaders/PostProcessing/ApplyShaperLookupTable.azsl @@ -362,6 +389,8 @@ set(FILES Shaders/PostProcessing/BloomCompositeCS.shader Shaders/PostProcessing/BloomDownsampleCS.azsl Shaders/PostProcessing/BloomDownsampleCS.shader + Shaders/PostProcessing/ContrastAdaptiveSharpening.azsl + Shaders/PostProcessing/ContrastAdaptiveSharpening.shader Shaders/PostProcessing/ConvertToAcescg.azsl Shaders/PostProcessing/ConvertToAcescg.shader Shaders/PostProcessing/DepthDownsample.azsl @@ -418,6 +447,19 @@ set(FILES Shaders/PostProcessing/MSAAResolveCustom.shader Shaders/PostProcessing/MSAAResolveDepth.azsl Shaders/PostProcessing/MSAAResolveDepth.shader + Shaders/PostProcessing/NewDepthOfFieldCommon.azsli + Shaders/PostProcessing/NewDepthOfFieldComposite.azsl + Shaders/PostProcessing/NewDepthOfFieldComposite.shader + Shaders/PostProcessing/NewDepthOfFieldDownsample.azsl + Shaders/PostProcessing/NewDepthOfFieldDownsample.shader + Shaders/PostProcessing/NewDepthOfFieldFilterLarge.azsl + Shaders/PostProcessing/NewDepthOfFieldFilterLarge.shader + Shaders/PostProcessing/NewDepthOfFieldFilterSmall.azsl + Shaders/PostProcessing/NewDepthOfFieldFilterSmall.shader + Shaders/PostProcessing/NewDepthOfFieldTile3x3.azsl + Shaders/PostProcessing/NewDepthOfFieldTile3x3.shader + Shaders/PostProcessing/NewDepthOfFieldTileReduce.azsl + Shaders/PostProcessing/NewDepthOfFieldTileReduce.shader Shaders/PostProcessing/OutputTransform.azsl Shaders/PostProcessing/OutputTransform.shader Shaders/PostProcessing/ScreenSpaceSubsurfaceScatteringCS.azsl @@ -434,13 +476,17 @@ set(FILES Shaders/PostProcessing/SMAAUtils.azsli Shaders/PostProcessing/SsaoCompute.azsl Shaders/PostProcessing/SsaoCompute.shader + Shaders/PostProcessing/Taa.azsl + Shaders/PostProcessing/Taa.shader Shaders/PostProcessing/UniformColor.azsl Shaders/PostProcessing/UniformColor.shader Shaders/Reflections/ReflectionCommon.azsli Shaders/Reflections/ReflectionComposite.azsl Shaders/Reflections/ReflectionComposite.shader + Shaders/Reflections/ReflectionComposite_nomsaa.azsl Shaders/Reflections/ReflectionGlobalFullscreen.azsl Shaders/Reflections/ReflectionGlobalFullscreen.shader + Shaders/Reflections/ReflectionGlobalFullscreen_nomsaa.azsl Shaders/Reflections/ReflectionProbeBlendWeight.azsl Shaders/Reflections/ReflectionProbeBlendWeight.shader Shaders/Reflections/ReflectionProbeRenderCommon.azsli @@ -469,6 +515,9 @@ set(FILES Shaders/Shadow/KawaseShadowBlur.shader Shaders/Shadow/Shadowmap.azsl Shaders/Shadow/Shadowmap.shader + Shaders/Shadow/ShadowmapCommon.azsli + Shaders/Shadow/ShadowmapSkin.azsl + Shaders/Shadow/ShadowmapSkin.shader Shaders/SkinnedMesh/LinearSkinningCS.azsl Shaders/SkinnedMesh/LinearSkinningCS.shader Shaders/SkinnedMesh/LinearSkinningPassSRG.azsli diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h index 769c7b95a6..fa987a7156 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h @@ -157,6 +157,12 @@ namespace AZ //! Sets whether the directional shadowmap should use receiver plane bias. //! This attempts to reduce shadow acne when using large pcf filters. virtual void SetShadowReceiverPlaneBiasEnabled(LightHandle handle, bool enable) = 0; + + //! Reduces acne by applying a small amount of bias along shadow-space z. + virtual void SetShadowBias(LightHandle handle, float bias) = 0; + + //! Reduces acne by biasing the shadowmap lookup along the geometric normal. + virtual void SetNormalShadowBias(LightHandle handle, float normalShadowBias) = 0; }; } // namespace Render } // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/PostProcess/ColorGrading/HDRColorGradingParams.inl b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/PostProcess/ColorGrading/HDRColorGradingParams.inl index c5865b0cfe..f41994fa77 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/PostProcess/ColorGrading/HDRColorGradingParams.inl +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/PostProcess/ColorGrading/HDRColorGradingParams.inl @@ -22,6 +22,7 @@ AZ_GFX_VEC3_PARAM(ColorFilterSwatch, m_colorFilterSwatch, AZ::Vector3(1.0f, 0.5f AZ_GFX_FLOAT_PARAM(WhiteBalanceWeight, m_whiteBalanceWeight, 0.0) AZ_GFX_FLOAT_PARAM(WhiteBalanceKelvin, m_whiteBalanceKelvin, 6600.0) AZ_GFX_FLOAT_PARAM(WhiteBalanceTint, m_whiteBalanceTint, 0.0) +AZ_GFX_FLOAT_PARAM(WhiteBalanceLuminancePreservation, m_whiteBalanceLuminancePreservation, 1.0) AZ_GFX_FLOAT_PARAM(SplitToneWeight, m_splitToneWeight, 0.0) AZ_GFX_FLOAT_PARAM(SplitToneBalance, m_splitToneBalance, 0.0) diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Shadows/ProjectedShadowFeatureProcessorInterface.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Shadows/ProjectedShadowFeatureProcessorInterface.h index 46560f435d..1517d655bb 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Shadows/ProjectedShadowFeatureProcessorInterface.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Shadows/ProjectedShadowFeatureProcessorInterface.h @@ -48,11 +48,13 @@ namespace AZ::Render virtual void SetAspectRatio(ShadowId id, float aspectRatio) = 0; //! Sets the field of view for the shadow in radians in the Y direction. virtual void SetFieldOfViewY(ShadowId id, float fieldOfView) = 0; - //! Sets the maximum resolution of the shadow map + //! Sets the maximum resolution of the shadow map. virtual void SetShadowmapMaxResolution(ShadowId id, ShadowmapSize size) = 0; - //! Sets the shadow bias + //! Sets the shadow bias. virtual void SetShadowBias(ShadowId id, float bias) = 0; - //! Sets the shadow filter method + //! Sets the normal shadow bias. + virtual void SetNormalShadowBias(ShadowId id, float normalShadowBias) = 0; + //! Sets the shadow filter method. virtual void SetShadowFilterMethod(ShadowId id, ShadowFilterMethod method) = 0; //! Sets the sample count for filtering of the shadow boundary, max 64. virtual void SetFilteringSampleCount(ShadowId id, uint16_t count) = 0; diff --git a/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp b/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp index 4ddf609f41..2e5db39880 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -250,6 +251,10 @@ namespace AZ passSystem->AddPassCreator(Name("DepthOfFieldReadBackFocusDepthPass"), &DepthOfFieldReadBackFocusDepthPass::Create); passSystem->AddPassCreator(Name("DepthOfFieldWriteFocusDepthFromGpuPass"), &DepthOfFieldWriteFocusDepthFromGpuPass::Create); + passSystem->AddPassCreator(Name("NewDepthOfFieldParentPass"), &NewDepthOfFieldParentPass::Create); + passSystem->AddPassCreator(Name("NewDepthOfFieldTileReducePass"), &NewDepthOfFieldTileReducePass::Create); + passSystem->AddPassCreator(Name("NewDepthOfFieldFilterPass"), &NewDepthOfFieldFilterPass::Create); + // Add FastDepthAwareBlur passes passSystem->AddPassCreator(Name("FastDepthAwareBlurHorPass"), &FastDepthAwareBlurHorPass::Create); passSystem->AddPassCreator(Name("FastDepthAwareBlurVerPass"), &FastDepthAwareBlurVerPass::Create); diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp index 0a9f3480ad..6c24b7d35b 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp @@ -589,6 +589,24 @@ namespace AZ m_shadowProperties.GetData(handle.GetIndex()).m_isReceiverPlaneBiasEnabled = enable; } + void DirectionalLightFeatureProcessor::SetShadowBias(LightHandle handle, float bias) + { + for (auto& it : m_shadowData) + { + it.second.GetData(handle.GetIndex()).m_shadowBias = bias; + } + m_shadowBufferNeedsUpdate = true; + } + + void DirectionalLightFeatureProcessor::SetNormalShadowBias(LightHandle handle, float normalShadowBias) + { + for (auto& it : m_shadowData) + { + it.second.GetData(handle.GetIndex()).m_normalShadowBias = normalShadowBias; + } + m_shadowBufferNeedsUpdate = true; + } + void DirectionalLightFeatureProcessor::OnRenderPipelineAdded(RPI::RenderPipelinePtr pipeline) { PrepareForChangingRenderPipelineAndCameraView(); @@ -1522,10 +1540,6 @@ namespace AZ for (uint16_t cascadeIndex = 0; cascadeIndex < GetCascadeCount(handle); ++cascadeIndex) { - const Matrix4x4& worldToLightClipMatrix = property.m_segments.at(cameraView)[cascadeIndex].m_view->GetWorldToClipMatrix(); - const Matrix4x4 depthBiasMatrix = Shadow::GetClipToShadowmapTextureMatrix() * worldToLightClipMatrix; - shadowData.m_depthBiasMatrices[cascadeIndex] = depthBiasMatrix; - const Matrix4x4& lightViewToLightClipMatrix = property.m_segments.at(cameraView)[cascadeIndex].m_view->GetViewToClipMatrix(); const Matrix4x4 lightViewToShadowmapMatrix = Shadow::GetClipToShadowmapTextureMatrix() * lightViewToLightClipMatrix; shadowData.m_lightViewToShadowmapMatrices[cascadeIndex] = lightViewToShadowmapMatrix; diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h index 3c1ff8eabd..8d7a9d76e4 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h @@ -72,12 +72,6 @@ namespace AZ // [GFX TODO][ATOM-15172] Look into compacting struct DirectionalLightShadowData struct DirectionalLightShadowData { - AZStd::array m_depthBiasMatrices = - { { - Matrix4x4::CreateIdentity(), - Matrix4x4::CreateIdentity(), - Matrix4x4::CreateIdentity(), - Matrix4x4::CreateIdentity() } }; AZStd::array m_lightViewToShadowmapMatrices = { { Matrix4x4::CreateIdentity(), @@ -97,13 +91,19 @@ namespace AZ float m_boundaryScale = 0.f; uint32_t m_shadowmapSize = 1; // width and height of shadowmap uint32_t m_cascadeCount = 1; - uint32_t m_predictionSampleCount = 0; + // Reduce acne by applying a small amount of bias to apply along shadow-space z. + float m_shadowBias = 0.0f; + // Reduces acne by biasing the shadowmap lookup along the geometric normal. + float m_normalShadowBias; uint32_t m_filteringSampleCount = 0; uint32_t m_debugFlags = 0; uint32_t m_shadowFilterMethod = 0; float m_far_minus_near = 0; + float m_padding[3]; }; + static_assert(sizeof(DirectionalLightShadowData) % 16 == 0); // Structured buffers need alignment to be a multiple of 16 bytes. + class DirectionalLightFeatureProcessor final : public DirectionalLightFeatureProcessorInterface { @@ -218,6 +218,8 @@ namespace AZ void SetShadowFilterMethod(LightHandle handle, ShadowFilterMethod method) override; void SetFilteringSampleCount(LightHandle handle, uint16_t count) override; void SetShadowReceiverPlaneBiasEnabled(LightHandle handle, bool enable) override; + void SetShadowBias(LightHandle handle, float bias) override; + void SetNormalShadowBias(LightHandle handle, float normalShadowBias) override; const Data::Instance GetLightBuffer() const; uint32_t GetLightCount() const; diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.cpp b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.cpp index 88266ba789..a3347d0d84 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.cpp @@ -43,6 +43,7 @@ m_whiteBalanceWeightIndex.Reset(); m_whiteBalanceKelvinIndex.Reset(); m_whiteBalanceTintIndex.Reset(); + m_whiteBalanceLuminancePreservationIndex.Reset(); m_splitToneBalanceIndex.Reset(); m_splitToneWeightIndex.Reset(); @@ -96,7 +97,7 @@ m_shaderResourceGroup->SetConstant(m_whiteBalanceWeightIndex, settings->GetWhiteBalanceWeight()); m_shaderResourceGroup->SetConstant(m_whiteBalanceKelvinIndex, settings->GetWhiteBalanceKelvin()); m_shaderResourceGroup->SetConstant(m_whiteBalanceTintIndex, settings->GetWhiteBalanceTint()); - + m_shaderResourceGroup->SetConstant(m_whiteBalanceLuminancePreservationIndex, settings->GetWhiteBalanceLuminancePreservation()); m_shaderResourceGroup->SetConstant(m_splitToneBalanceIndex, settings->GetSplitToneBalance()); m_shaderResourceGroup->SetConstant(m_splitToneWeightIndex, settings->GetSplitToneWeight()); m_shaderResourceGroup->SetConstant(m_splitToneShadowsColorIndex, AZ::Vector4(settings->GetSplitToneShadowsColor())); diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.h b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.h index dc706b8501..b89fa04531 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.h +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.h @@ -55,6 +55,7 @@ namespace AZ RHI::ShaderInputNameIndex m_whiteBalanceWeightIndex = "m_whiteBalanceWeight"; RHI::ShaderInputNameIndex m_whiteBalanceKelvinIndex = "m_whiteBalanceKelvin"; RHI::ShaderInputNameIndex m_whiteBalanceTintIndex = "m_whiteBalanceTint"; + RHI::ShaderInputNameIndex m_whiteBalanceLuminancePreservationIndex = "m_whiteBalanceLuminancePreservation"; RHI::ShaderInputNameIndex m_splitToneBalanceIndex = "m_splitToneBalance"; RHI::ShaderInputNameIndex m_splitToneWeightIndex = "m_splitToneWeight"; diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/NewDepthOfFieldPasses.cpp b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/NewDepthOfFieldPasses.cpp new file mode 100644 index 0000000000..69ee2a6751 --- /dev/null +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/NewDepthOfFieldPasses.cpp @@ -0,0 +1,176 @@ +/* + * 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 + +namespace AZ +{ + namespace Render + { + + // Must match the struct in NewDepthOfFieldCommon.azsli + struct NewDepthOfFieldConstants + { + static constexpr uint32_t numberOfLoops = 3; + static constexpr float loopCounts[] = { 8.0f, 16.0f, 24.0f }; + + AZStd::array m_samplePositions; // XY are sample positions (normalized so max lenght is 1) + // Z is the length of XY (0 - 1) + // W is unused + }; + + // --- Depth of Field Parent Pass --- + + RPI::Ptr NewDepthOfFieldParentPass::Create(const RPI::PassDescriptor& descriptor) + { + RPI::Ptr pass = aznew NewDepthOfFieldParentPass(descriptor); + return AZStd::move(pass); + } + + NewDepthOfFieldParentPass::NewDepthOfFieldParentPass(const RPI::PassDescriptor& descriptor) + : RPI::ParentPass(descriptor) + { } + + bool NewDepthOfFieldParentPass::IsEnabled() const + { + if (!ParentPass::IsEnabled()) + { + return false; + } + RPI::Scene* scene = GetScene(); + if (!scene) + { + return false; + } + PostProcessFeatureProcessor* fp = scene->GetFeatureProcessor(); + AZ::RPI::ViewPtr view = GetRenderPipeline()->GetDefaultView(); + if (!fp) + { + return false; + } + PostProcessSettings* postProcessSettings = fp->GetLevelSettingsFromView(view); + if (!postProcessSettings) + { + return false; + } + DepthOfFieldSettings* dofSettings = postProcessSettings->GetDepthOfFieldSettings(); + return (dofSettings != nullptr) && dofSettings->GetEnabled(); + } + + void NewDepthOfFieldParentPass::FrameBeginInternal(FramePrepareParams params) + { + RPI::Scene* scene = GetScene(); + PostProcessFeatureProcessor* fp = scene->GetFeatureProcessor(); + AZ::RPI::ViewPtr view = GetRenderPipeline()->GetDefaultView(); + if (fp) + { + PostProcessSettings* postProcessSettings = fp->GetLevelSettingsFromView(view); + if (postProcessSettings) + { + DepthOfFieldSettings* dofSettings = postProcessSettings->GetDepthOfFieldSettings(); + if (dofSettings) + { + dofSettings->SetValuesToViewSrg(view->GetShaderResourceGroup()); + } + } + } + + ParentPass::FrameBeginInternal(params); + } + + // --- Tile Reduce Pass --- + + RPI::Ptr NewDepthOfFieldTileReducePass::Create(const RPI::PassDescriptor& descriptor) + { + RPI::Ptr pass = aznew NewDepthOfFieldTileReducePass(descriptor); + return AZStd::move(pass); + } + + NewDepthOfFieldTileReducePass::NewDepthOfFieldTileReducePass(const RPI::PassDescriptor& descriptor) + : RPI::ComputePass(descriptor) + { + // Though this is a fullscreen pass, the shader computes 16x16 tiles with groups of 8x8 threads, + // each thread outputting to a single pixel in the tiled min/max texture + m_isFullscreenPass = false; + } + + void NewDepthOfFieldTileReducePass::FrameBeginInternal(FramePrepareParams params) + { + AZ_Assert(GetOutputCount() > 0, "NewDepthOfFieldTileReducePass: No output bindings!"); + RPI::PassAttachment* outputAttachment = GetOutputBinding(0).m_attachment.get(); + + AZ_Assert(outputAttachment != nullptr, "NewDepthOfFieldTileReducePass: Output binding has no attachment!"); + RHI::Size outputSize = outputAttachment->m_descriptor.m_image.m_size; + + // The algorithm outputs the min/max CoC values from a 16x16 region using 8x8 threads + u32 targetThreadCountX = outputSize.m_width * 8; + u32 targetThreadCountY = outputSize.m_height * 8; + SetTargetThreadCounts(targetThreadCountX, targetThreadCountY, 1); + + RPI::ComputePass::FrameBeginInternal(params); + } + + // --- Filter Pass --- + + RPI::Ptr NewDepthOfFieldFilterPass::Create(const RPI::PassDescriptor& descriptor) + { + RPI::Ptr pass = aznew NewDepthOfFieldFilterPass(descriptor); + return AZStd::move(pass); + } + + NewDepthOfFieldFilterPass::NewDepthOfFieldFilterPass(const RPI::PassDescriptor& descriptor) + : RPI::FullscreenTrianglePass(descriptor) + { } + + void NewDepthOfFieldFilterPass::FrameBeginInternal(FramePrepareParams params) + { + NewDepthOfFieldConstants dofConstants; + + uint32_t sampleIndex = 0; + + // Calculate all the offset positions + for (uint32_t loop = 0; loop < NewDepthOfFieldConstants::numberOfLoops; ++loop) + { + float radius = (loop + 1.0f) / float(NewDepthOfFieldConstants::numberOfLoops); + float loopCount = NewDepthOfFieldConstants::loopCounts[loop]; + + float angleStep = Constants::TwoPi / loopCount; + + // Every other loop slightly rotate sample ring so they don't line up + float angle = (loop & 1) ? (angleStep * 0.5f) : 0; + + for (float i = 0.0f; i < loopCount; ++i) + { + Vector2 pos = Vector2::CreateFromAngle(angle); + pos = pos * radius; + + dofConstants.m_samplePositions[sampleIndex][0] = pos.GetX(); + dofConstants.m_samplePositions[sampleIndex][1] = pos.GetY(); + dofConstants.m_samplePositions[sampleIndex][2] = radius; + dofConstants.m_samplePositions[sampleIndex][3] = 0.0f; + + ++sampleIndex; + angle += angleStep; + } + } + + m_shaderResourceGroup->SetConstant(m_constantsIndex, dofConstants); + + RPI::FullscreenTrianglePass::FrameBeginInternal(params); + } + + } // namespace Render +} // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/NewDepthOfFieldPasses.h b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/NewDepthOfFieldPasses.h new file mode 100644 index 0000000000..159f91e744 --- /dev/null +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/NewDepthOfFieldPasses.h @@ -0,0 +1,119 @@ +/* + * 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 AZ +{ + namespace Render + { + // Technique + // + // 1. This Depth of Field technique starts by downsampling the lighting buffer and calculating + // the circle of confusion (CoC) for each downsampled pixel. + // + // 2. It then computes the min and max CoC for tiles of 16x16 pixels + // + // 3. It expands the min and max in a 3x3 region (twice, so 5x5 at the end) so that each tile + // tile pixel has the min and max CoCs of the 5x5 tile region around it + // + // 4. We perform a 48 tap scatter-as-gather blur around each pixel + // + // 5. We perform a follow up 8 tap scatter-as-gather blur to fill the holes from the first blur + // + // 6. We composite the blurred half resolution image onto the full resolution lighting buffer + // + // See http://advances.realtimerendering.com/s2013/Sousa_Graphics_Gems_CryENGINE3.pptx + // for a more detailed explanation. + // + // Notes: The name NewDepthOfField is in contrast to the previously implemented depth of field method + // That method will be removed in a follow up change and at that point NewDepthOfField will be renamed + // to simple DepthOfField. + + //! Parent pass for the new depth of field technique + //! Main updates the view srg via the depth of field settings + //! And enables/disables all depth of field passes based on component activation + class NewDepthOfFieldParentPass final + : public RPI::ParentPass + { + AZ_RPI_PASS(NewDepthOfFieldParentPass); + + public: + AZ_RTTI(AZ::Render::NewDepthOfFieldParentPass, "{71F4998B-447C-4BAC-A5BE-2D2850FABB57}", AZ::RPI::ParentPass); + AZ_CLASS_ALLOCATOR(NewDepthOfFieldParentPass, SystemAllocator, 0); + virtual ~NewDepthOfFieldParentPass() = default; + + static RPI::Ptr Create(const RPI::PassDescriptor& descriptor); + + bool IsEnabled() const override; + + protected: + // Behavior functions override... + void FrameBeginInternal(FramePrepareParams params) override; + + private: + NewDepthOfFieldParentPass(const RPI::PassDescriptor& descriptor); + }; + + + //! Need a class for the tile reduce pass because it dispatches a non-trivial number of threads + class NewDepthOfFieldTileReducePass final + : public RPI::ComputePass + { + AZ_RPI_PASS(NewDepthOfFieldTileReducePass); + + public: + AZ_RTTI(AZ::Render::NewDepthOfFieldTileReducePass, "{2E072695-0847-43A6-9BE4-D6D85CFFBA41}", AZ::RPI::ComputePass); + AZ_CLASS_ALLOCATOR(NewDepthOfFieldTileReducePass, SystemAllocator, 0); + virtual ~NewDepthOfFieldTileReducePass() = default; + + static RPI::Ptr Create(const RPI::PassDescriptor& descriptor); + + protected: + // Behavior functions override... + void FrameBeginInternal(FramePrepareParams params) override; + + private: + NewDepthOfFieldTileReducePass(const RPI::PassDescriptor& descriptor); + }; + + + //! Filter pass used to render the bokeh blur effect on downsampled image buffer + //! This class is used for both the large filter and the small filter + //! It's main purpose is calculating the sample positions and setting srg constants + class NewDepthOfFieldFilterPass final + : public RPI::FullscreenTrianglePass + { + AZ_RPI_PASS(NewDepthOfFieldFilterPass); + + public: + AZ_RTTI(AZ::Render::NewDepthOfFieldFilterPass, "{F8A98E53-1A50-4178-A6EB-2BD0148C038B}", AZ::RPI::FullscreenTrianglePass); + AZ_CLASS_ALLOCATOR(NewDepthOfFieldFilterPass, SystemAllocator, 0); + virtual ~NewDepthOfFieldFilterPass() = default; + + static RPI::Ptr Create(const RPI::PassDescriptor& descriptor); + + protected: + // Behavior functions override... + void FrameBeginInternal(FramePrepareParams params) override; + + private: + NewDepthOfFieldFilterPass(const RPI::PassDescriptor& descriptor); + + // SRG binding indices... + AZ::RHI::ShaderInputNameIndex m_constantsIndex = "m_dofConstants"; + }; + + + } // namespace Render +} // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/TaaPass.cpp b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/TaaPass.cpp index 779f02cba0..574d959c07 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/TaaPass.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/TaaPass.cpp @@ -168,7 +168,6 @@ namespace AZ::Render // The ImageViewDescriptor must be specified to make sure the frame graph compiler doesn't treat this as a transient image. RHI::ImageViewDescriptor viewDesc = RHI::ImageViewDescriptor::Create(imageDesc.m_format, 0, 0); viewDesc.m_aspectFlags = RHI::ImageAspectFlags::Color; - viewDesc.m_overrideBindFlags = RHI::ImageBindFlags::ShaderReadWrite; // The full path name is needed for the attachment image so it's not deduplicated from accumulation images in different pipelines. AZStd::string imageName = RPI::ConcatPassString(GetPathName(), attachment->m_path); diff --git a/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.cpp index da92cc04e7..7c0b3563c7 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.cpp @@ -151,6 +151,14 @@ namespace AZ::Render shadowProperty.m_bias = bias; } + void ProjectedShadowFeatureProcessor::SetNormalShadowBias(ShadowId id, float normalShadowBias) + { + AZ_Assert(id.IsValid(), "Invalid ShadowId passed to ProjectedShadowFeatureProcessor::SetNormalShadowBias()."); + + ShadowProperty& shadowProperty = GetShadowPropertyFromShadowId(id); + shadowProperty.m_normalShadowBias = normalShadowBias; + } + void ProjectedShadowFeatureProcessor::SetShadowmapMaxResolution(ShadowId id, ShadowmapSize size) { AZ_Assert(id.IsValid(), "Invalid ShadowId passed to ProjectedShadowFeatureProcessor::SetShadowmapMaxResolution()."); diff --git a/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.h index 6269166827..fafcb25a08 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.h +++ b/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.h @@ -48,6 +48,7 @@ namespace AZ::Render void SetFieldOfViewY(ShadowId id, float fieldOfViewYRadians) override; void SetShadowmapMaxResolution(ShadowId id, ShadowmapSize size) override; void SetShadowBias(ShadowId id, float bias) override; + void SetNormalShadowBias(ShadowId id, float normalShadowBias) override; void SetShadowFilterMethod(ShadowId id, ShadowFilterMethod method) override; void SetFilteringSampleCount(ShadowId id, uint16_t count) override; void SetShadowProperties(ShadowId id, const ProjectedShadowDescriptor& descriptor) override; @@ -64,10 +65,10 @@ namespace AZ::Render uint32_t m_shadowmapArraySlice = 0; // array slice who has shadowmap in the atlas. uint32_t m_shadowFilterMethod = 0; // filtering method of shadows. float m_boundaryScale = 0.f; // the half of boundary of lit/shadowed areas. (in degrees) - uint32_t m_predictionSampleCount = 0; // sample count to judge whether it is on the shadow boundary or not. uint32_t m_filteringSampleCount = 0; AZStd::array m_unprojectConstants = { {0, 0} }; float m_bias; + float m_normalShadowBias; float m_esmExponent = 87.0f; float m_padding[3]; }; @@ -78,6 +79,7 @@ namespace AZ::Render ProjectedShadowDescriptor m_desc; RPI::ViewPtr m_shadowmapView; float m_bias = 0.1f; + float m_normalShadowBias = 0.0f; ShadowId m_shadowId; }; diff --git a/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake b/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake index c875f7c177..184460b210 100644 --- a/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake +++ b/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake @@ -243,6 +243,8 @@ set(FILES Source/PostProcessing/LookModificationTransformPass.h Source/PostProcessing/LuminanceHistogramGeneratorPass.h Source/PostProcessing/LuminanceHistogramGeneratorPass.cpp + Source/PostProcessing/NewDepthOfFieldPasses.cpp + Source/PostProcessing/NewDepthOfFieldPasses.h Source/PostProcessing/PostProcessingShaderOptionBase.cpp Source/PostProcessing/PostProcessingShaderOptionBase.h Source/PostProcessing/SMAABasePass.cpp diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/ShaderInputNameIndex.h b/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/ShaderInputNameIndex.h index 03062afd52..5e9b1a9c77 100644 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/ShaderInputNameIndex.h +++ b/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/ShaderInputNameIndex.h @@ -67,6 +67,10 @@ namespace AZ bool IsInitialized() const; void AssetInialized() const; + // Retrieves the underlying name. Should only be used for debug purposes like printing the name when we fail to bind to the SRG. + // All regular functionality should go through the above functions. + const Name& GetNameForDebug() const; + private: enum class IndexType : u32 diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI/SwapChain.h b/Gems/Atom/RHI/Code/Include/Atom/RHI/SwapChain.h index 14e9968e42..97ac3baa90 100644 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI/SwapChain.h +++ b/Gems/Atom/RHI/Code/Include/Atom/RHI/SwapChain.h @@ -14,92 +14,86 @@ namespace AZ { namespace RHI { - /** - * The platform-independent swap chain base class. Swap chains contain a "chain" of images which - * map to a platform-specific window, displayed on a physical monitor. The user is allowed - * to adjust the swap chain outside of the current FrameScheduler frame. Doing so within a frame scheduler - * frame results in undefined behavior. - * - * The frame scheduler controls presentation of the swap chain. The user may attach a swap chain to a scope - * in order to render to the current image. - */ + //! The platform-independent swap chain base class. Swap chains contain a "chain" of images which + //! map to a platform-specific window, displayed on a physical monitor. The user is allowed + //! to adjust the swap chain outside of the current FrameScheduler frame. Doing so within a frame scheduler + //! frame results in undefined behavior. + //! + //! The frame scheduler controls presentation of the swap chain. The user may attach a swap chain to a scope + //! in order to render to the current image. class SwapChain : public ImagePoolBase { public: + AZ_RTTI(SwapChain, "{888B64A5-D956-406F-9C33-CF6A54FC41B0}", Object); + virtual ~SwapChain(); - /// Initializes the swap chain, making it ready for attachment. + //! Initializes the swap chain, making it ready for attachment. ResultCode Init(RHI::Device& device, const SwapChainDescriptor& descriptor); - /// Presents the swap chain to the display, and rotates the images. + //! Presents the swap chain to the display, and rotates the images. void Present(); - /** - * Sets the vertical sync interval for the swap chain. - * 0 - No vsync. - * N - Sync to every N vertical refresh. - * - * A value of 1 syncs to the refresh rate of the monitor. - */ + //! Sets the vertical sync interval for the swap chain. + //! 0 - No vsync. + //! N - Sync to every N vertical refresh. + //! + //! A value of 1 syncs to the refresh rate of the monitor. void SetVerticalSyncInterval(uint32_t verticalSyncInterval); - /** - * Resizes the display resolution of the swap chain. Ideally, this matches the platform window - * resolution. Typically, the resize operation will occur in reaction to a platform window size - * change. Takes effect immediately and results in a GPU pipeline flush. - */ + //! Resizes the display resolution of the swap chain. Ideally, this matches the platform window + //! resolution. Typically, the resize operation will occur in reaction to a platform window size + //! change. Takes effect immediately and results in a GPU pipeline flush. ResultCode Resize(const SwapChainDimensions& dimensions); - /// Returns the number of images in the swap chain. + //! Returns the number of images in the swap chain. uint32_t GetImageCount() const; - /// Returns the current image index of the swap chain. + //! Returns the current image index of the swap chain. uint32_t GetCurrentImageIndex() const; - /// Returns the current image of the swap chain. + //! Returns the current image of the swap chain. Image* GetCurrentImage() const; - /// Returns the image associated with the provided index, where the total number of images - /// is given by GetImageCount(). + //! Returns the image associated with the provided index, where the total number of images + //! is given by GetImageCount(). Image* GetImage(uint32_t index) const; - /// Returns the ID used for the SwapChain's attachment + //! Returns the ID used for the SwapChain's attachment const AttachmentId& GetAttachmentId() const; - /// Returns the descriptor provided when initializing the swap chain. + //! Returns the descriptor provided when initializing the swap chain. const RHI::SwapChainDescriptor& GetDescriptor() const override final; - //! \return True if the swap chain prefers to use exclusive full screen mode. + //! Returns True if the swap chain prefers to use exclusive full screen mode. virtual bool IsExclusiveFullScreenPreferred() const { return false; } - //! \return True if the swap chain prefers exclusive full screen mode and it is currently true, false otherwise. + //! Returns True if the swap chain prefers exclusive full screen mode and it is currently true, false otherwise. virtual bool GetExclusiveFullScreenState() const { return false; } - //! \return True if the swap chain prefers exclusive full screen mode and a transition happened, false otherwise. + //! Return True if the swap chain prefers exclusive full screen mode and a transition happened, false otherwise. virtual bool SetExclusiveFullScreenState([[maybe_unused]]bool fullScreenState) { return false; } - AZ_RTTI(SwapChain, "{888B64A5-D956-406F-9C33-CF6A54FC41B0}", Object); - protected: SwapChain(); struct InitImageRequest { - /// Pointer to the image to initialize. + //! Pointer to the image to initialize. Image* m_image = nullptr; - /// Index of the image in the swap chain. + //! Index of the image in the swap chain. uint32_t m_imageIndex = 0; - /// Descriptor for the image. + //! Descriptor for the image. ImageDescriptor m_descriptor; }; ////////////////////////////////////////////////////////////////////////// // ResourcePool Overrides - /// Called when the pool is shutting down. + //! Called when the pool is shutting down. void ShutdownInternal() override; ////////////////////////////////////////////////////////////////////////// @@ -111,32 +105,29 @@ namespace AZ ////////////////////////////////////////////////////////////////////////// // Platform API - /// Called when the swap chain is initializing. + //! Called when the swap chain is initializing. virtual ResultCode InitInternal(RHI::Device& device, const SwapChainDescriptor& descriptor, SwapChainDimensions* nativeDimensions) = 0; - /// called when the swap chain is initializing an image. + //! called when the swap chain is initializing an image. virtual ResultCode InitImageInternal(const InitImageRequest& request) = 0; - /// Called when the swap chain is resizing. + //! Called when the swap chain is resizing. virtual ResultCode ResizeInternal(const SwapChainDimensions& dimensions, SwapChainDimensions* nativeDimensions) = 0; - /// Called when the swap chain is presenting the currently swap image. - /// Returns the index of the current image after the swap. + //! Called when the swap chain is presenting the currently swap image. + //! Returns the index of the current image after the swap. virtual uint32_t PresentInternal() = 0; - virtual void SetVerticalSyncIntervalInternal(uint32_t previousVerticalSyncInterval) - { - AZ_UNUSED(previousVerticalSyncInterval); - } + virtual void SetVerticalSyncIntervalInternal([[maybe_unused]]uint32_t previousVerticalSyncInterval) {} ////////////////////////////////////////////////////////////////////////// SwapChainDescriptor m_descriptor; - /// Images corresponding to each image in the swap chain. + //! Images corresponding to each image in the swap chain. AZStd::vector> m_images; - /// The current image index. + //! The current image index. uint32_t m_currentImageIndex = 0; }; } diff --git a/Gems/Atom/RHI/Code/Source/RHI.Reflect/ShaderInputNameIndex.cpp b/Gems/Atom/RHI/Code/Source/RHI.Reflect/ShaderInputNameIndex.cpp index 2eb23895f8..b7ed2dbfe7 100644 --- a/Gems/Atom/RHI/Code/Source/RHI.Reflect/ShaderInputNameIndex.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI.Reflect/ShaderInputNameIndex.cpp @@ -227,5 +227,11 @@ namespace AZ } + const Name& ShaderInputNameIndex::GetNameForDebug() const + { + AZ_Assert(HasName(), "GetNameForDebug() called on ShaderInputNameIndex that doesn't have a name set. Please initialize it with a name.", m_name.GetCStr()); + return m_name; + } + } } diff --git a/Gems/Atom/RHI/Code/Source/RHI/SwapChain.cpp b/Gems/Atom/RHI/Code/Source/RHI/SwapChain.cpp index b0501d937d..ff1f0e69a6 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/SwapChain.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/SwapChain.cpp @@ -55,7 +55,7 @@ namespace AZ if (resultCode == ResultCode::Success) { m_descriptor = descriptor; - // Ovewrite descriptor dimensions with the native ones (the ones assigned by the platform) returned by InitInternal. + // Overwrite descriptor dimensions with the native ones (the ones assigned by the platform) returned by InitInternal. m_descriptor.m_dimensions = nativeDimensions; m_images.reserve(m_descriptor.m_dimensions.m_imageCount); @@ -129,8 +129,8 @@ namespace AZ while (m_images.size() > static_cast(m_descriptor.m_dimensions.m_imageCount)) { m_images.pop_back(); - } - + } + InitImageRequest request; RHI::ImageDescriptor& imageDescriptor = request.m_descriptor; diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphExecuteGroupHandlerBase.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphExecuteGroupHandlerBase.cpp index e85c86f3f4..9c1bb896f9 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphExecuteGroupHandlerBase.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphExecuteGroupHandlerBase.cpp @@ -27,6 +27,7 @@ namespace AZ { EndInternal(); m_device->GetCommandQueueContext().GetCommandQueue(m_hardwareQueueClass).ExecuteWork(AZStd::move(m_workRequest)); + m_isExecuted = true; } bool FrameGraphExecuteGroupHandlerBase::IsComplete() const @@ -42,6 +43,11 @@ namespace AZ return true; } + bool FrameGraphExecuteGroupHandlerBase::IsExecuted() const + { + return m_isExecuted; + } + template void InsertWorkRequestElements(T& destination, const T& source) { diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphExecuteGroupHandlerBase.h b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphExecuteGroupHandlerBase.h index a0aa6d465a..1aad409667 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphExecuteGroupHandlerBase.h +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphExecuteGroupHandlerBase.h @@ -41,6 +41,7 @@ namespace AZ void End(); bool IsComplete() const; + bool IsExecuted() const; protected: virtual RHI::ResultCode InitInternal(Device& device, const AZStd::vector& executeGroups) = 0; @@ -52,6 +53,7 @@ namespace AZ ExecuteWorkRequest m_workRequest; RHI::HardwareQueueClass m_hardwareQueueClass = RHI::HardwareQueueClass::Graphics; AZStd::vector m_executeGroups; + bool m_isExecuted = false; }; } } diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphExecuter.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphExecuter.cpp index 7165b5c305..45fe9344a9 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphExecuter.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphExecuter.cpp @@ -177,8 +177,8 @@ namespace AZ auto findIter = m_groupHandlers.find(group.GetGroupId()); AZ_Assert(findIter != m_groupHandlers.end(), "Could not find group handler for groupId %d", group.GetGroupId().GetIndex()); FrameGraphExecuteGroupHandlerBase* handler = findIter->second.get(); - // Wait until all execute groups of the handler has finished. - if (handler->IsComplete()) + // Wait until all execute groups of the handler has finished and also make sure that the handler itself hasn't executed already (which is possible for parallel encoding). + if (!handler->IsExecuted() && handler->IsComplete()) { // This will execute the recorded work into the queue. handler->End(); diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/SwapChain.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/SwapChain.cpp index 7040defca8..bef2b154e1 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/SwapChain.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/SwapChain.cpp @@ -61,13 +61,12 @@ namespace AZ void SwapChain::SetVerticalSyncIntervalInternal(uint32_t previousVsyncInterval) { - uint32_t verticalSyncInterval = GetDescriptor().m_verticalSyncInterval; - if (verticalSyncInterval == 0 || previousVsyncInterval == 0) + if (GetDescriptor().m_verticalSyncInterval == 0 || previousVsyncInterval == 0) { // The presentation mode may change when transitioning to or from a vsynced presentation mode // In this case, the swapchain must be recreated. InvalidateNativeSwapChain(); - BuildNativeSwapChain(GetDescriptor().m_dimensions, verticalSyncInterval); + CreateSwapchain(); } } @@ -85,46 +84,21 @@ namespace AZ RHI::DeviceObject::Init(baseDevice); auto& device = static_cast(GetDevice()); - RHI::SwapChainDimensions swapchainDimensions = descriptor.m_dimensions; + m_dimensions = descriptor.m_dimensions; + result = BuildSurface(descriptor); RETURN_RESULT_IF_UNSUCCESSFUL(result); - if (!ValidateSurfaceDimensions(swapchainDimensions)) - { - swapchainDimensions.m_imageHeight = AZStd::clamp(swapchainDimensions.m_imageHeight, m_surfaceCapabilities.minImageExtent.height, m_surfaceCapabilities.maxImageExtent.height); - swapchainDimensions.m_imageWidth = AZStd::clamp(swapchainDimensions.m_imageWidth, m_surfaceCapabilities.minImageExtent.width, m_surfaceCapabilities.maxImageExtent.width); - AZ_Printf("Vulkan", "Resizing swapchain from (%d, %d) to (%d, %d).", - static_cast(descriptor.m_dimensions.m_imageWidth), static_cast(descriptor.m_dimensions.m_imageHeight), - static_cast(swapchainDimensions.m_imageWidth), static_cast(swapchainDimensions.m_imageHeight)); - } auto& presentationQueue = device.GetCommandQueueContext().GetOrCreatePresentationCommandQueue(*this); m_presentationQueue = &presentationQueue; - result = BuildNativeSwapChain(swapchainDimensions, descriptor.m_verticalSyncInterval); - RETURN_RESULT_IF_UNSUCCESSFUL(result); - uint32_t imageCount = 0; - VkResult vkResult = vkGetSwapchainImagesKHR(device.GetNativeDevice(), m_nativeSwapChain, &imageCount, nullptr); - AssertSuccess(vkResult); - RETURN_RESULT_IF_UNSUCCESSFUL(ConvertResult(vkResult)); - - m_swapchainNativeImages.resize(imageCount); - // Retrieve the native images of the swapchain so they are - // available when we init the Images in InitImageInternal - vkResult = vkGetSwapchainImagesKHR(device.GetNativeDevice(), m_nativeSwapChain, &imageCount, m_swapchainNativeImages.data()); - AssertSuccess(vkResult); - RETURN_RESULT_IF_UNSUCCESSFUL(ConvertResult(vkResult)); - - // Acquire the first image - uint32_t imageIndex = 0; - result = AcquireNewImage(&imageIndex); + result = CreateSwapchain(); RETURN_RESULT_IF_UNSUCCESSFUL(result); if (nativeDimensions) { // Fill out the real swapchain dimensions to return - nativeDimensions->m_imageCount = imageCount; - nativeDimensions->m_imageHeight = swapchainDimensions.m_imageHeight; - nativeDimensions->m_imageWidth = swapchainDimensions.m_imageWidth; + *nativeDimensions = m_dimensions; nativeDimensions->m_imageFormat = ConvertFormat(m_surfaceFormat.format); } @@ -165,51 +139,24 @@ namespace AZ RHI::ResultCode SwapChain::ResizeInternal(const RHI::SwapChainDimensions& dimensions, RHI::SwapChainDimensions* nativeDimensions) { auto& device = static_cast(GetDevice()); + m_dimensions = dimensions; InvalidateNativeSwapChain(); - InvalidateSurface(); - RHI::SwapChainDimensions resizeDimensions = dimensions; - BuildSurface(GetDescriptor()); - if (!ValidateSurfaceDimensions(dimensions)) - { - resizeDimensions.m_imageHeight = AZStd::clamp(dimensions.m_imageHeight, m_surfaceCapabilities.minImageExtent.height, m_surfaceCapabilities.maxImageExtent.height); - resizeDimensions.m_imageWidth = AZStd::clamp(dimensions.m_imageWidth, m_surfaceCapabilities.minImageExtent.width, m_surfaceCapabilities.maxImageExtent.width); - AZ_Printf("Vulkan", "Resizing swapchain from (%d, %d) to (%d, %d).", - static_cast(dimensions.m_imageWidth), static_cast(dimensions.m_imageHeight), - static_cast(resizeDimensions.m_imageWidth), static_cast(resizeDimensions.m_imageHeight)); - } auto& presentationQueue = device.GetCommandQueueContext().GetOrCreatePresentationCommandQueue(*this); m_presentationQueue = &presentationQueue; - BuildNativeSwapChain(resizeDimensions, GetDescriptor().m_verticalSyncInterval); - - resizeDimensions.m_imageCount = 0; - VkResult vkResult = vkGetSwapchainImagesKHR(device.GetNativeDevice(), m_nativeSwapChain, &resizeDimensions.m_imageCount, nullptr); - RETURN_RESULT_IF_UNSUCCESSFUL(ConvertResult(vkResult)); - - m_swapchainNativeImages.resize(resizeDimensions.m_imageCount); - // Retrieve the native images of the swapchain so they are - // available when we init the Images in InitImageInternal - vkResult = vkGetSwapchainImagesKHR(device.GetNativeDevice(), m_nativeSwapChain, &resizeDimensions.m_imageCount, m_swapchainNativeImages.data()); - RETURN_RESULT_IF_UNSUCCESSFUL(ConvertResult(vkResult)); - - // Do not recycle the semaphore because they may not ever get signaled and since - // we can't recycle Vulkan semaphores we just delete them. - m_currentFrameContext.m_imageAvailableSemaphore->SetRecycleValue(false); - m_currentFrameContext.m_presentableSemaphore->SetRecycleValue(false); - - // Acquire the first image - uint32_t imageIndex = 0; - AcquireNewImage(&imageIndex); + CreateSwapchain(); if (nativeDimensions) { - *nativeDimensions = resizeDimensions; + *nativeDimensions = m_dimensions; // [ATOM-4840] This is a workaround when the windows is minimized (0x0 size). // Add proper support to handle this case. - nativeDimensions->m_imageHeight = AZStd::max(resizeDimensions.m_imageHeight, 1u); - nativeDimensions->m_imageWidth = AZStd::max(resizeDimensions.m_imageWidth, 1u); + nativeDimensions->m_imageHeight = AZStd::max(m_dimensions.m_imageHeight, 1u); + nativeDimensions->m_imageWidth = AZStd::max(m_dimensions.m_imageWidth, 1u); + + nativeDimensions->m_imageFormat = ConvertFormat(m_surfaceFormat.format); } return RHI::ResultCode::Success; @@ -271,20 +218,48 @@ namespace AZ info.pImageIndices = &imageIndex; info.pResults = nullptr; - [[maybe_unused]] const VkResult result = vkQueuePresentKHR(vulkanQueue->GetNativeQueue(), &info); - - // Resizing window cause recreation of SwapChain after calling this method, - // so VK_SUBOPTIMAL_KHR or VK_ERROR_OUT_OF_DATE_KHR should not happen at this point. - AZ_Assert(result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR, "Failed to present swapchain %s", GetName().GetCStr()); - AZ_Warning("Vulkan", result != VK_SUBOPTIMAL_KHR, "Suboptimal presentation of swapchain %s", GetName().GetCStr()); + const VkResult result = vkQueuePresentKHR(vulkanQueue->GetNativeQueue(), &info); + + // Vulkan's definition of the two types of errors. + // VK_ERROR_OUT_OF_DATE_KHR: "A surface has changed in such a way that it is no longer compatible with the swapchain, + // and further presentation requests using the swapchain will fail. Applications must query the new surface + // properties and recreate their swapchain if they wish to continue presenting to the surface." + // VK_SUBOPTIMAL_KHR: "A swapchain no longer matches the surface properties exactly, but can still be used to + // present to the surface successfully." + // + // These result values may occur after resizing or some window operation. We should update the surface info and recreate the swapchain. + // VK_SUBOPTIMAL_KHR is treated as success, but we better update the surface info as well. + if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) + { + InvalidateNativeSwapChain(); + CreateSwapchain(); + } + else + { + // Other errors are: + // VK_ERROR_OUT_OF_HOST_MEMORY + // VK_ERROR_OUT_OF_DEVICE_MEMORY + // VK_ERROR_DEVICE_LOST + // VK_ERROR_SURFACE_LOST_KHR + // VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT + AZ_Assert(result == VK_SUCCESS, "Unhandled error for swapchain presentation."); + } }; m_presentationQueue->QueueCommand(AZStd::move(presentCommand)); uint32_t acquiredImageIndex = GetCurrentImageIndex(); - AcquireNewImage(&acquiredImageIndex); - - return acquiredImageIndex; + RHI::ResultCode result = AcquireNewImage(&acquiredImageIndex); + if (result == RHI::ResultCode::Fail) + { + InvalidateNativeSwapChain(); + CreateSwapchain(); + return 0; + } + else + { + return acquiredImageIndex; + } } RHI::ResultCode SwapChain::BuildSurface(const RHI::SwapChainDescriptor& descriptor) @@ -293,15 +268,8 @@ namespace AZ surfaceDesc.m_windowHandle = descriptor.m_window; RHI::Ptr surface = WSISurface::Create(); const RHI::ResultCode result = surface->Init(surfaceDesc); - if (result == RHI::ResultCode::Success) - { - m_surface = surface; - auto& device = static_cast(GetDevice()); - const auto& physicalDevice = static_cast(device.GetPhysicalDevice()); - VkResult vkResult = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice.GetNativePhysicalDevice(), m_surface->GetNativeSurface(), &m_surfaceCapabilities); - AssertSuccess(vkResult); - RETURN_RESULT_IF_UNSUCCESSFUL(ConvertResult(vkResult)); - } + RETURN_RESULT_IF_UNSUCCESSFUL(result); + m_surface = surface; return result; } @@ -373,6 +341,21 @@ namespace AZ return supportedModes[0]; } + VkSurfaceCapabilitiesKHR SwapChain::GetSurfaceCapabilities() + { + AZ_Assert(m_surface, "Surface has not been initialized."); + + auto& device = static_cast(GetDevice()); + const auto& physicalDevice = static_cast(device.GetPhysicalDevice()); + + VkSurfaceCapabilitiesKHR surfaceCapabilities; + VkResult vkResult = vkGetPhysicalDeviceSurfaceCapabilitiesKHR( + physicalDevice.GetNativePhysicalDevice(), m_surface->GetNativeSurface(), &surfaceCapabilities); + AssertSuccess(vkResult); + + return surfaceCapabilities; + } + VkCompositeAlphaFlagBitsKHR SwapChain::GetSupportedCompositeAlpha() const { VkFlags supportedModesBits = m_surfaceCapabilities.supportedCompositeAlpha; @@ -394,15 +377,9 @@ namespace AZ return VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; } - RHI::ResultCode SwapChain::BuildNativeSwapChain(const RHI::SwapChainDimensions& dimensions, uint32_t verticalSyncInterval) + RHI::ResultCode SwapChain::BuildNativeSwapChain(const RHI::SwapChainDimensions& dimensions) { AZ_Assert(m_nativeSwapChain == VK_NULL_HANDLE, "Vulkan's native SwapChain has been initialized already."); - auto& device = static_cast(GetDevice()); - auto& queueContext = device.GetCommandQueueContext(); - const VkExtent2D extent = { - dimensions.m_imageWidth, - dimensions.m_imageHeight - }; AZ_Assert(m_surface, "Surface is null."); if (!ValidateSurfaceDimensions(dimensions)) @@ -410,7 +387,11 @@ namespace AZ AZ_Assert(false, "Swapchain dimensions are not supported."); return RHI::ResultCode::InvalidArgument; } - m_surfaceFormat = GetSupportedSurfaceFormat(dimensions.m_imageFormat); + + auto& device = static_cast(GetDevice()); + auto& queueContext = device.GetCommandQueueContext(); + const VkExtent2D extent = { dimensions.m_imageWidth, dimensions.m_imageHeight }; + // If the graphic queue is the same as the presentation queue, then we will always acquire // 1 image at the same time. If it's another queue, we will have 2 at the same time (while the other queue // presents the image) @@ -441,11 +422,11 @@ namespace AZ createInfo.imageArrayLayers = 1; // non-stereoscopic createInfo.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; - createInfo.queueFamilyIndexCount = static_cast(familyIndices.size()); + createInfo.queueFamilyIndexCount = aznumeric_cast(familyIndices.size()); createInfo.pQueueFamilyIndices = familyIndices.empty() ? nullptr : familyIndices.data(); createInfo.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; - createInfo.compositeAlpha = GetSupportedCompositeAlpha(); - createInfo.presentMode = GetSupportedPresentMode(verticalSyncInterval); + createInfo.compositeAlpha = m_compositeAlphaFlagBits; + createInfo.presentMode = m_presentMode; createInfo.clipped = VK_FALSE; createInfo.oldSwapchain = VK_NULL_HANDLE; @@ -467,9 +448,6 @@ namespace AZ VK_NULL_HANDLE, acquiredImageIndex); - // Resizing window cause recreation of SwapChain before calling this method, - // so VK_SUBOPTIMAL_KHR or VK_ERROR_OUT_OF_DATE_KHR should not happen. - AssertSuccess(vkResult); RHI::ResultCode result = ConvertResult(vkResult); RETURN_RESULT_IF_UNSUCCESSFUL(result); @@ -484,6 +462,7 @@ namespace AZ } m_currentFrameContext.m_imageAvailableSemaphore = imageAvailableSemaphore; m_currentFrameContext.m_presentableSemaphore = semaphoreAllocator.Allocate(); + return result; } @@ -502,5 +481,70 @@ namespace AZ m_nativeSwapChain = VK_NULL_HANDLE; } } + + RHI::ResultCode SwapChain::CreateSwapchain() + { + auto& device = static_cast(GetDevice()); + + m_surfaceCapabilities = GetSurfaceCapabilities(); + m_surfaceFormat = GetSupportedSurfaceFormat(GetDescriptor().m_dimensions.m_imageFormat); + m_presentMode = GetSupportedPresentMode(GetDescriptor().m_verticalSyncInterval); + m_compositeAlphaFlagBits = GetSupportedCompositeAlpha(); + + if (!ValidateSurfaceDimensions(m_dimensions)) + { + uint32_t oldHeight = m_dimensions.m_imageHeight; + uint32_t oldWidth = m_dimensions.m_imageWidth; + m_dimensions.m_imageHeight = AZStd::clamp( + m_dimensions.m_imageHeight, + m_surfaceCapabilities.minImageExtent.height, + m_surfaceCapabilities.maxImageExtent.height); + m_dimensions.m_imageWidth = AZStd::clamp( + m_dimensions.m_imageWidth, + m_surfaceCapabilities.minImageExtent.width, + m_surfaceCapabilities.maxImageExtent.width); + AZ_Printf( + "Vulkan", "Resizing swapchain from (%u, %u) to (%u, %u).", + oldWidth, oldHeight, m_dimensions.m_imageWidth, m_dimensions.m_imageHeight); + } + + RHI::ResultCode result = BuildNativeSwapChain(m_dimensions); + RETURN_RESULT_IF_UNSUCCESSFUL(result); + AZ_TracePrintf("Swapchain", "Swapchain created. Width: %u, Height: %u.", m_dimensions.m_imageWidth, m_dimensions.m_imageHeight); + + // Do not recycle the semaphore because they may not ever get signaled and since + // we can't recycle Vulkan semaphores we just delete them. + if (m_currentFrameContext.m_imageAvailableSemaphore) + { + m_currentFrameContext.m_imageAvailableSemaphore->SetRecycleValue(false); + } + if (m_currentFrameContext.m_presentableSemaphore) + { + m_currentFrameContext.m_presentableSemaphore->SetRecycleValue(false); + } + + m_dimensions.m_imageCount = 0; + VkResult vkResult = vkGetSwapchainImagesKHR(device.GetNativeDevice(), m_nativeSwapChain, &m_dimensions.m_imageCount, nullptr); + AssertSuccess(vkResult); + RETURN_RESULT_IF_UNSUCCESSFUL(ConvertResult(vkResult)); + + m_swapchainNativeImages.resize(m_dimensions.m_imageCount); + + // Retrieve the native images of the swapchain so they are + // available when we init the images in InitImageInternal + vkResult = vkGetSwapchainImagesKHR( + device.GetNativeDevice(), m_nativeSwapChain, &m_dimensions.m_imageCount, m_swapchainNativeImages.data()); + AssertSuccess(vkResult); + RETURN_RESULT_IF_UNSUCCESSFUL(ConvertResult(vkResult)); + AZ_TracePrintf("Swapchain", "Obtained presentable images."); + + // Acquire the first image + uint32_t imageIndex = 0; + result = AcquireNewImage(&imageIndex); + RETURN_RESULT_IF_UNSUCCESSFUL(result); + AZ_TracePrintf("Swapchain", "Acquired the first image."); + + return RHI::ResultCode::Success; + } } } diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/SwapChain.h b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/SwapChain.h index 14dfd34b08..ee2ff3c207 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/SwapChain.h +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/SwapChain.h @@ -70,24 +70,48 @@ namespace AZ ////////////////////////////////////////////////////////////////////// RHI::ResultCode BuildSurface(const RHI::SwapChainDescriptor& descriptor); + + //! Returns true is the swapchain dimensions are supported by the current surface. bool ValidateSurfaceDimensions(const RHI::SwapChainDimensions& dimensions); + //! Returns the corresponding Vulkan format that is supported by the surface. + //! If such format is not found, return the first supported format from the surface. VkSurfaceFormatKHR GetSupportedSurfaceFormat(const RHI::Format format) const; + //! Returns the correct presentation mode. + //! If verticalSyncInterval is non-zero, returns VK_PRESENT_MODE_FIFO_KHR. + //! Otherwise, choose preferred mode if they are supported. + //! If not, the first supported present mode is returned. VkPresentModeKHR GetSupportedPresentMode(uint32_t verticalSyncInterval) const; + //! Returns the preferred alpha compositing modes if they are supported. + //! If not, error will be reported. VkCompositeAlphaFlagBitsKHR GetSupportedCompositeAlpha() const; - RHI::ResultCode BuildNativeSwapChain(const RHI::SwapChainDimensions& dimensions, uint32_t verticalSyncInterval); + //! Returns the current surface capabilities. + VkSurfaceCapabilitiesKHR GetSurfaceCapabilities(); + //! Create the swapchain when initializing, or + //! swapchain is no longer compatible or is sub-optimal with the surface. + RHI::ResultCode CreateSwapchain(); + //! Build underlying Vulkan swapchain. + RHI::ResultCode BuildNativeSwapChain(const RHI::SwapChainDimensions& dimensions); + //! Retrieve the index of the next available presentable image. RHI::ResultCode AcquireNewImage(uint32_t* acquiredImageIndex); + //! Destroy the surface. void InvalidateSurface(); + //! Destroy the old swapchain. void InvalidateNativeSwapChain(); - VkSwapchainKHR m_nativeSwapChain = VK_NULL_HANDLE; RHI::Ptr m_surface; + VkSwapchainKHR m_nativeSwapChain = VK_NULL_HANDLE; CommandQueue* m_presentationQueue = nullptr; - VkSurfaceFormatKHR m_surfaceFormat = {}; - VkSurfaceCapabilitiesKHR m_surfaceCapabilities; - FrameContext m_currentFrameContext; + //! Swapchain data + VkSurfaceFormatKHR m_surfaceFormat = {}; + VkSurfaceCapabilitiesKHR m_surfaceCapabilities = {}; + VkPresentModeKHR m_presentMode = {}; + VkCompositeAlphaFlagBitsKHR m_compositeAlphaFlagBits = {}; + AZStd::vector m_swapchainNativeImages; + RHI::SwapChainDimensions m_dimensions; + struct SwapChainBarrier { VkPipelineStageFlags m_srcPipelineStages = 0; @@ -95,8 +119,6 @@ namespace AZ VkImageMemoryBarrier m_barrier = {}; bool m_isValid = false; } m_swapChainBarrier; - - AZStd::vector m_swapchainNativeImages; }; } } diff --git a/Gems/Atom/RPI/Assets/Shader/DecomposeMsImage.shader b/Gems/Atom/RPI/Assets/Shader/DecomposeMsImage.shader index 424c5ad6e3..943de52767 100644 --- a/Gems/Atom/RPI/Assets/Shader/DecomposeMsImage.shader +++ b/Gems/Atom/RPI/Assets/Shader/DecomposeMsImage.shader @@ -1,5 +1,5 @@ { - "Source": "DecomposeMsImage", + "Source": "DecomposeMsImage.azsl", "ProgramSettings": { diff --git a/Gems/Atom/RPI/Assets/Shader/ImagePreview.shader b/Gems/Atom/RPI/Assets/Shader/ImagePreview.shader index d87a4506a2..9a3594e2dd 100644 --- a/Gems/Atom/RPI/Assets/Shader/ImagePreview.shader +++ b/Gems/Atom/RPI/Assets/Shader/ImagePreview.shader @@ -1,5 +1,5 @@ { - "Source" : "ImagePreview", + "Source" : "ImagePreview.azsl", "DepthStencilState" : { "Depth" : { "Enable" : false, "CompareFunc" : "GreaterEqual" } diff --git a/Gems/Atom/RPI/Assets/ShaderLib/Atom/RPI/Math.azsli b/Gems/Atom/RPI/Assets/ShaderLib/Atom/RPI/Math.azsli index 80f8a6cc36..6ffdb6e815 100644 --- a/Gems/Atom/RPI/Assets/ShaderLib/Atom/RPI/Math.azsli +++ b/Gems/Atom/RPI/Assets/ShaderLib/Atom/RPI/Math.azsli @@ -31,6 +31,8 @@ void swap(inout float a, inout float b) b = c; } +// ---------- Power ----------- + float Pow2(float x) { return x * x; @@ -48,6 +50,44 @@ float Pow5(float x) { return x * Pow4(x); } + +// ---------- Min & Max ----------- + +float min3(float a, float b, float c) +{ + return min(min(a, b), c); +} +float min4(float a, float b, float c, float d) +{ + return min(min3(a, b, c), d); +} +float max3(float a, float b, float c) +{ + return max(max(a, b), c); +} +float max4(float a, float b, float c, float d) +{ + return max(max3(a, b, c), d); +} + +float min3(float3 abc) +{ + return min3(abc.x, abc.y, abc.z); +} +float min4(float4 abcd) +{ + return min4(abcd.x, abcd.y, abcd.z, abcd.w); +} +float max3(float3 abc) +{ + return max3(abc.x, abc.y, abc.z); +} +float max4(float4 abcd) +{ + return max4(abcd.x, abcd.y, abcd.z, abcd.w); +} + + // ---------- Intersection ----------- // a simple ray sphere intersection function, didn't take limited precision diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyValueSerializer.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyValueSerializer.h index befbb7c990..29371618cf 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyValueSerializer.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyValueSerializer.h @@ -24,6 +24,13 @@ namespace AZ AZ_RTTI(AZ::RPI::JsonMaterialPropertyValueSerializer, "{A52B1ED8-C849-4269-9AA7-9D0814D2EC59}", BaseJsonSerializer); AZ_CLASS_ALLOCATOR_DECL; + //! A LoadContext object must be passed down to the serializer via JsonDeserializerContext::GetMetadata().Add(...) + struct LoadContext + { + AZ_TYPE_INFO(JsonMaterialPropertyValueSerializer::LoadContext, "{5E0A891A-27F6-4AD7-88A5-B9EA50F88B45}"); + uint32_t m_materialTypeVersion; //!< The version number from the .materialtype file + }; + JsonSerializationResult::Result Load(void* outputValue, const Uuid& outputValueTypeId, const rapidjson::Value& inputValue, JsonDeserializerContext& context) override; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h index 02607a7954..a67477f061 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h @@ -50,7 +50,7 @@ namespace AZ AZStd::string m_parentMaterial; //!< The immediate parent of this material - uint32_t m_propertyLayoutVersion = 0; //!< The version of the property layout, defined in the material type, which was used to configure this material + uint32_t m_materialTypeVersion = 0; //!< The version of the material type that was used to configure this material struct Property { @@ -64,6 +64,18 @@ namespace AZ PropertyGroupMap m_properties; + enum class ApplyVersionUpdatesResult + { + Failed, + NoUpdates, + UpdatesApplied + }; + + //! Checks the material type version and potentially applies a series of property changes (most common are simple property renames) + //! based on the MaterialTypeAsset's version update procedure. + //! @param materialSourceFilePath Indicates the path of the .material file that the MaterialSourceData represents. Used for resolving file-relative paths. + ApplyVersionUpdatesResult ApplyVersionUpdates(AZStd::string_view materialSourceFilePath = ""); + //! Creates a MaterialAsset from the MaterialSourceData content. //! @param assetId ID for the MaterialAsset //! @param materialSourceFilePath Indicates the path of the .material file that the MaterialSourceData represents. Used for resolving file-relative paths. diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialTypeSourceData.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialTypeSourceData.h index 04b6222404..1234b15f95 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialTypeSourceData.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialTypeSourceData.h @@ -13,6 +13,7 @@ #include #include #include +#include namespace AZ { @@ -119,12 +120,36 @@ namespace AZ using PropertyList = AZStd::vector; + struct VersionUpdatesRenameOperationDefinition + { + AZ_TYPE_INFO(AZ::RPI::MaterialTypeSourceData::VersionUpdatesRenameOperationDefinition, "{F2295489-E15A-46CC-929F-8D42DEDBCF14}"); + + AZStd::string m_operation; + + AZStd::string m_renameFrom; + AZStd::string m_renameTo; + }; + + // TODO: Support script operations--At that point, we'll likely need to replace VersionUpdatesRenameOperationDefinition with a more generic + // data structure that has a custom JSON serialize. We will only be supporting rename for now. + using VersionUpdateActions = AZStd::vector; + + struct VersionUpdateDefinition + { + AZ_TYPE_INFO(AZ::RPI::MaterialTypeSourceData::VersionUpdateDefinition, "{2C9D3B91-0585-4BC9-91D2-4CF0C71BC4B7}"); + + uint32_t m_toVersion; + VersionUpdateActions m_actions; + }; + + using VersionUpdates = AZStd::vector; + struct PropertyLayout { AZ_TYPE_INFO(AZ::RPI::MaterialTypeSourceData::PropertyLayout, "{AE53CF3F-5C3B-44F5-B2FB-306F0EB06393}"); - - //! Indicates the version of the set of available properties. Can be used to detect materials that might need to be updated. - uint32_t m_version = 0; + + //! This field is unused, and has been replaced by MaterialTypeSourceData::m_version below. It is kept for legacy file compatibility to suppress warnings and errors. + uint32_t m_versionOld = 0; //! List of groups that will contain the available properties AZStd::vector m_groups; @@ -135,6 +160,11 @@ namespace AZ AZStd::string m_description; + //! Version 1 is the default and should not contain any version update. + uint32_t m_version = 1; + + VersionUpdates m_versionUpdates; + PropertyLayout m_propertyLayout; //! A list of shader variants that are always used at runtime; they cannot be turned off @@ -153,7 +183,12 @@ namespace AZ const GroupDefinition* FindGroup(AZStd::string_view groupName) const; - const PropertyDefinition* FindProperty(AZStd::string_view groupName, AZStd::string_view propertyName) const; + //! Searches for a specific property. + //! Note this function can find properties using old versions of the property name; in that case, + //! the name in the returned PropertyDefinition* will not match the @propertyName that was searched for. + //! @param materialTypeVersion indicates the version number of the property name being passed in. Only renames above this version number will be applied. + //! @return the requested property, or null if it could not be found + const PropertyDefinition* FindProperty(AZStd::string_view groupName, AZStd::string_view propertyName, uint32_t materialTypeVersion = 0) const; //! Construct a complete list of group definitions, including implicit groups, arranged in the same order as the source data //! Groups with the same name will be consolidated into a single entry @@ -179,6 +214,11 @@ namespace AZ bool ConvertPropertyValueToSourceDataFormat(const PropertyDefinition& propertyDefinition, MaterialPropertyValue& propertyValue) const; Outcome> CreateMaterialTypeAsset(Data::AssetId assetId, AZStd::string_view materialTypeSourceFilePath = "", bool elevateWarnings = true) const; + + //! Possibly renames @propertyId based on the material version update steps. + //! @param materialTypeVersion indicates the version number of the property name being passed in. Only renames above this version number will be applied. + //! @return true if the property was renamed + bool ApplyPropertyRenames(MaterialPropertyId& propertyId, uint32_t materialTypeVersion = 0) const; }; //! The wrapper class for derived material functors. diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassAttachment.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassAttachment.h index babcf752e9..d60a5a8064 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassAttachment.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassAttachment.h @@ -10,6 +10,8 @@ #include #include +#include + #include namespace AZ @@ -145,7 +147,11 @@ namespace AZ //! Name of the SRG member this binds to (see PassSlot::m_shaderInputName for more details) Name m_shaderInputName = Name("AutoBind"); - + + //! Name index of the SRG constant to which, if specified, we automatically calculate + //! and bind the image dimensions (if this binding is of type image) + RHI::ShaderInputNameIndex m_shaderImageDimensionsNameIndex; + //! Whether binding is an input, output or inputOutput PassSlotType m_slotType = PassSlotType::Uninitialized; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/RenderPass.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/RenderPass.h index cbff297021..5fb90d044e 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/RenderPass.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/RenderPass.h @@ -119,7 +119,7 @@ namespace AZ private: // Helper function that binds a single attachment to the pass shader resource group - void BindAttachment(const RHI::FrameGraphCompileContext& context, const PassAttachmentBinding& binding, int16_t& imageIndex, int16_t& bufferIndex); + void BindAttachment(const RHI::FrameGraphCompileContext& context, PassAttachmentBinding& binding, int16_t& imageIndex, int16_t& bufferIndex); // Helper function to get the query by the scope index and query type RHI::Ptr GetQuery(ScopeQueryType queryType); diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialAsset.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialAsset.h index 6ca3bca652..2a1de6debd 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialAsset.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialAsset.h @@ -22,6 +22,7 @@ namespace UnitTest { class MaterialTests; + class MaterialAssetTests; } namespace AZ @@ -42,10 +43,12 @@ namespace AZ , public MaterialReloadNotificationBus::Handler , public AssetInitBus::Handler { + friend class MaterialVersionUpdate; friend class MaterialAssetCreator; friend class MaterialAssetHandler; friend class MaterialAssetCreatorCommon; friend class UnitTest::MaterialTests; + friend class UnitTest::MaterialAssetTests; public: AZ_RTTI(MaterialAsset, "{522C7BE0-501D-463E-92C6-15184A2B7AD8}", AZ::Data::AssetData); @@ -119,6 +122,10 @@ namespace AZ //! from m_materialTypeAsset. void RealignPropertyValuesAndNames(); + //! Checks the material type version and potentially applies a series of property changes (most common are simple property renames) + //! based on the MaterialTypeAsset's version update procedure. + void ApplyVersionUpdates(); + //! Called by asset creators to assign the asset to a ready state. void SetReady(); @@ -143,6 +150,10 @@ namespace AZ //! If empty, this implies that m_propertyValues is aligned with the entries in m_materialPropertiesLayout. AZStd::vector m_propertyNames; + //! The materialTypeVersion this materialAsset was based of. If the versions do not match at runtime when a + //! materialTypeAsset is loaded, an update will be performed on m_propertyNames if populated. + uint32_t m_materialTypeVersion = 1; + //! A flag to determine if m_propertyValues needs to be aligned with MaterialPropertiesLayout. Set to true whenever //! m_materialTypeAsset is reinitializing. bool m_isDirty = true; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAsset.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAsset.h index e705a8041b..9bc0e018d3 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAsset.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAsset.h @@ -18,6 +18,7 @@ #include #include #include +#include namespace AZ { @@ -123,6 +124,11 @@ namespace AZ //! Returns a map from the UV shader inputs to a custom name. MaterialUvNameMap GetUvNameMap() const; + //! Returns the version of the MaterialTypeAsset. + uint32_t GetVersion() const; + + const AZStd::vector& GetMaterialVersionUpdateList() const { return m_materialVersionUpdates; } + private: bool PostLoadInit() override; @@ -162,6 +168,12 @@ namespace AZ //! Index in @m_shaderCollection of the shader asset that contains the ObjectSrg. uint32_t m_objectSrgShaderIndex = InvalidShaderIndex; + //! The version of this MaterialTypeAsset. If the version is greater than 1, actions performed + //! to update this MaterialTypeAsset will be in m_materialVersionUpdateMap + uint32_t m_version = 1; + + //! Contains actions to perform for each material update version. + AZStd::vector m_materialVersionUpdates; }; class MaterialTypeAssetHandler : public AssetHandler diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAssetCreator.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAssetCreator.h index 70488245a2..5e5f94da6d 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAssetCreator.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAssetCreator.h @@ -38,6 +38,11 @@ namespace AZ void AddShader(const AZ::Data::Asset& shaderAsset, const ShaderVariantId& shaderVaraintId = ShaderVariantId{}, const AZ::Name& shaderTag = Uuid::CreateRandom().ToString()); void AddShader(const AZ::Data::Asset& shaderAsset, const AZ::Name& shaderTag); + //! Sets the version of the MaterialTypeAsset + void SetVersion(uint32_t version); + //! Adds a version update object into the MaterialTypeAsset + void AddVersionUpdate(const MaterialVersionUpdate& materialVersionUpdate); + //! Indicates that this MaterialType will own the specified shader option. //! Material-owned shader options can be connected to material properties (either directly or through functors). //! They cannot be accessed externally (for example, through the Material::SetSystemShaderOption() function). @@ -112,6 +117,7 @@ namespace AZ //! Saves the per-material SRG layout in m_shaderResourceGroupLayout for easier access void CacheMaterialSrgLayout(); + bool ValidateMaterialVersion(); bool ValidateBeginMaterialProperty(); bool ValidateEndMaterialProperty(); diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialVersionUpdate.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialVersionUpdate.h new file mode 100644 index 0000000000..eb71ad9cb7 --- /dev/null +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialVersionUpdate.h @@ -0,0 +1,62 @@ +/* + * 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 AZ +{ + namespace RPI + { + class MaterialAsset; + + // This class contains a toVersion and a list of actions to specify what operations were performed to upgrade a materialType. + class MaterialVersionUpdate + { + public: + AZ_TYPE_INFO(AZ::RPI::MaterialVersionUpdate, "{B36E7712-AED8-46AA-AFE0-01F8F884C44A}"); + + static void Reflect(ReflectContext* context); + + // At this time, the only supported operation is rename. If/when we add more actions in the future, + // we'll need to improve this, possibly with some virtual interface or union data. + struct RenamePropertyAction + { + AZ_TYPE_INFO(AZ::RPI::MaterialVersionUpdate::RenameAction, "{A1FBEB19-EA05-40F0-9700-57D048DF572B}"); + + static void Reflect(ReflectContext* context); + + AZ::Name m_fromPropertyId; + AZ::Name m_toPropertyId; + }; + + explicit MaterialVersionUpdate() = default; + explicit MaterialVersionUpdate(uint32_t toVersion); + + uint32_t GetVersion() const; + void SetVersion(uint32_t toVersion); + + //! Apply version updates to the given material asset. + //! @return true if any changes were made + bool ApplyVersionUpdates(MaterialAsset& materialAsset) const; + + using Actions = AZStd::vector; + const Actions& GetActions() const; + void AddAction(const RenamePropertyAction& action); + + private: + uint32_t m_toVersion; + Actions m_actions; + }; + + } // namespace RPI +} // namespace AZ diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Pass/PassAttachmentReflect.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Pass/PassAttachmentReflect.h index 1131f2ff70..0de3676abe 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Pass/PassAttachmentReflect.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Pass/PassAttachmentReflect.h @@ -87,6 +87,13 @@ namespace AZ //! The keyword "NoBind" means the slot will not bind it's attachment to the SRG Name m_shaderInputName = Name("AutoBind"); + //! Name of the shader resource group constant (must be float4) to which the pass can automatically bind the following: + //! X component = image width + //! Y component = image height + //! Z component = 1 / image width + //! W component = 1 / image height + Name m_shaderImageDimensionsName; + //! This is to specify an array index if the shader input is an array. //! e.g. Texture2DMS m_color[4]; uint16_t m_shaderInputArrayIndex = 0; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Builders/Material/MaterialBuilder.cpp b/Gems/Atom/RPI/Code/Source/RPI.Builders/Material/MaterialBuilder.cpp index 86ea8ab688..e9cebf29c7 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Builders/Material/MaterialBuilder.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Builders/Material/MaterialBuilder.cpp @@ -47,7 +47,7 @@ namespace AZ { AssetBuilderSDK::AssetBuilderDesc materialBuilderDescriptor; materialBuilderDescriptor.m_name = JobKey; - materialBuilderDescriptor.m_version = 109; // Changed "id" to "name" in serialization + materialBuilderDescriptor.m_version = 110; // Material version auto update feature materialBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.material", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard)); materialBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.materialtype", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard)); materialBuilderDescriptor.m_busId = azrtti_typeid(); @@ -287,6 +287,11 @@ namespace AZ return {}; } + if (MaterialSourceData::ApplyVersionUpdatesResult::Failed == material.GetValue().ApplyVersionUpdates(materialSourceFilePath)) + { + return {}; + } + auto materialAssetOutcome = material.GetValue().CreateMaterialAsset(Uuid::CreateRandom(), materialSourceFilePath, true); if (!materialAssetOutcome.IsSuccess()) { diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyValueSerializer.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyValueSerializer.cpp index 3b2d36451a..5e04365ffb 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyValueSerializer.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyValueSerializer.cpp @@ -62,6 +62,8 @@ namespace AZ return context.Report(JsonSerializationResult::Tasks::ReadField, JsonSerializationResult::Outcomes::Catastrophic, "Material type reference not found."); } + const JsonMaterialPropertyValueSerializer::LoadContext* loadContext = context.GetMetadata().Find(); + // Construct the full property name (groupName.propertyName) by parsing it from the JSON path string. size_t startPropertyName = context.GetPath().Get().rfind('/'); size_t startGroupName = context.GetPath().Get().rfind('/', startPropertyName-1); @@ -70,7 +72,7 @@ namespace AZ JSR::ResultCode result(JSR::Tasks::ReadField); - auto propertyDefinition = materialType->FindProperty(groupName, propertyName); + auto propertyDefinition = materialType->FindProperty(groupName, propertyName, loadContext->m_materialTypeVersion); if (!propertyDefinition) { AZStd::string message = AZStd::string::format("Property '%.*s.%.*s' not found in material type.", AZ_STRING_ARG(groupName), AZ_STRING_ARG(propertyName)); diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.cpp index f697f33a3f..5aed6b2993 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.cpp @@ -72,6 +72,62 @@ namespace AZ materialAssetCreator.SetPropertyValue(propertyId, entry.second); } } + + MaterialSourceData::ApplyVersionUpdatesResult MaterialSourceData::ApplyVersionUpdates(AZStd::string_view materialSourceFilePath) + { + AZStd::string materialTypeFullPath = AssetUtils::ResolvePathReference(materialSourceFilePath, m_materialType); + auto materialTypeSourceDataOutcome = MaterialUtils::LoadMaterialTypeSourceData(materialTypeFullPath); + if (!materialTypeSourceDataOutcome.IsSuccess()) + { + return ApplyVersionUpdatesResult::Failed; + } + + MaterialTypeSourceData materialTypeSourceData = materialTypeSourceDataOutcome.TakeValue(); + + if (m_materialTypeVersion == materialTypeSourceData.m_version) + { + return ApplyVersionUpdatesResult::NoUpdates; + } + + bool changesWereApplied = false; + + // Note that the only kind of property update currently supported is rename... + + for (auto& groupPair : m_properties) + { + PropertyMap& propertyMap = groupPair.second; + + PropertyMap newPropertyMap; + + for (auto& propertyPair : propertyMap) + { + MaterialPropertyId propertyId{groupPair.first, propertyPair.first}; + if (materialTypeSourceData.ApplyPropertyRenames(propertyId, m_materialTypeVersion)) + { + newPropertyMap[propertyId.GetPropertyName().GetStringView()] = propertyPair.second; + changesWereApplied = true; + } + else + { + newPropertyMap[propertyPair.first] = propertyPair.second; + } + } + + propertyMap = newPropertyMap; + } + + if (changesWereApplied) + { + AZ_Warning("MaterialSourceData", false, + "This material is based on version '%u' of '%s', but the material type is now at version '%u'. " + "Automatic updates are available. Consider updating the .material source file.", + m_materialTypeVersion, m_materialType.c_str(), materialTypeSourceData.m_version); + } + + m_materialTypeVersion = materialTypeSourceData.m_version; + + return changesWereApplied ? ApplyVersionUpdatesResult::UpdatesApplied : ApplyVersionUpdatesResult::NoUpdates; + } Outcome > MaterialSourceData::CreateMaterialAsset(Data::AssetId assetId, AZStd::string_view materialSourceFilePath, bool elevateWarnings, bool includeMaterialPropertyNames) const { diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceDataSerializer.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceDataSerializer.cpp index 5e4af07aae..2a504fc345 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceDataSerializer.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceDataSerializer.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -45,9 +46,9 @@ namespace AZ } result.Combine(ContinueLoadingFromJsonObjectField(&materialSourceData->m_description, azrtti_typeid(), inputValue, "description", context)); - result.Combine(ContinueLoadingFromJsonObjectField(&materialSourceData->m_materialType, azrtti_typeid(), inputValue, "materialType", context)); result.Combine(ContinueLoadingFromJsonObjectField(&materialSourceData->m_parentMaterial, azrtti_typeid(), inputValue, "parentMaterial", context)); - result.Combine(ContinueLoadingFromJsonObjectField(&materialSourceData->m_propertyLayoutVersion, azrtti_typeid(), inputValue, "propertyLayoutVersion", context)); + result.Combine(ContinueLoadingFromJsonObjectField(&materialSourceData->m_materialType, azrtti_typeid(), inputValue, "materialType", context)); + result.Combine(ContinueLoadingFromJsonObjectField(&materialSourceData->m_materialTypeVersion, azrtti_typeid(), inputValue, "materialTypeVersion", context)); if (materialSourceData->m_materialType.empty()) { @@ -118,6 +119,10 @@ namespace AZ context.GetMetadata().Add(AZStd::move(materialTypeData)); + JsonMaterialPropertyValueSerializer::LoadContext materialPropertyValueLoadContext; + materialPropertyValueLoadContext.m_materialTypeVersion = materialSourceData->m_materialTypeVersion; + context.GetMetadata().Add(materialPropertyValueLoadContext); + result.Combine(ContinueLoadingFromJsonObjectField(&materialSourceData->m_properties, azrtti_typeid(), inputValue, "properties", context)); if (result.GetProcessing() == JsonSerializationResult::Processing::Completed) @@ -146,9 +151,9 @@ namespace AZ JSR::ResultCode resultCode(JSR::Tasks::ReadField); resultCode.Combine(ContinueStoringToJsonObjectField(outputValue, "description", &materialSourceData->m_description, nullptr, azrtti_typeid(), context)); - resultCode.Combine(ContinueStoringToJsonObjectField(outputValue, "materialType", &materialSourceData->m_materialType, nullptr, azrtti_typeid(), context)); resultCode.Combine(ContinueStoringToJsonObjectField(outputValue, "parentMaterial", &materialSourceData->m_parentMaterial, nullptr, azrtti_typeid(), context)); - resultCode.Combine(ContinueStoringToJsonObjectField(outputValue, "propertyLayoutVersion", &materialSourceData->m_propertyLayoutVersion, nullptr, azrtti_typeid(), context)); + resultCode.Combine(ContinueStoringToJsonObjectField(outputValue, "materialType", &materialSourceData->m_materialType, nullptr, azrtti_typeid(), context)); + resultCode.Combine(ContinueStoringToJsonObjectField(outputValue, "materialTypeVersion", &materialSourceData->m_materialTypeVersion, nullptr, azrtti_typeid(), context)); resultCode.Combine(ContinueStoringToJsonObjectField(outputValue, "properties", &materialSourceData->m_properties, nullptr, azrtti_typeid(), context)); return context.Report(resultCode, "Processed material."); diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp index 1cc57f4d47..74250647c3 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,23 @@ namespace AZ serializeContext->RegisterGenericType(); + serializeContext->Class() + ->Version(1) + ->Field("op", &VersionUpdatesRenameOperationDefinition::m_operation) + ->Field("from", &VersionUpdatesRenameOperationDefinition::m_renameFrom) + ->Field("to", &VersionUpdatesRenameOperationDefinition::m_renameTo) + ; + + serializeContext->RegisterGenericType(); + + serializeContext->Class() + ->Version(1) + ->Field("toVersion", &VersionUpdateDefinition::m_toVersion) + ->Field("actions", &VersionUpdateDefinition::m_actions) + ; + + serializeContext->RegisterGenericType(); + serializeContext->Class() ->Version(2) ->Field("file", &ShaderVariantReferenceData::m_shaderFilePath) @@ -67,8 +85,8 @@ namespace AZ ; serializeContext->Class() - ->Version(1) - ->Field("version", &PropertyLayout::m_version) + ->Version(2) // Material Version Update + ->Field("version", &PropertyLayout::m_versionOld) ->Field("groups", &PropertyLayout::m_groups) ->Field("properties", &PropertyLayout::m_properties) ; @@ -76,8 +94,10 @@ namespace AZ serializeContext->RegisterGenericType(); serializeContext->Class() - ->Version(3) + ->Version(4) // Material Version Update ->Field("description", &MaterialTypeSourceData::m_description) + ->Field("version", &MaterialTypeSourceData::m_version) + ->Field("versionUpdates", &MaterialTypeSourceData::m_versionUpdates) ->Field("propertyLayout", &MaterialTypeSourceData::m_propertyLayout) ->Field("shaders", &MaterialTypeSourceData::m_shaderCollection) ->Field("functors", &MaterialTypeSourceData::m_materialFunctorSourceData) @@ -110,7 +130,38 @@ namespace AZ return nullptr; } - const MaterialTypeSourceData::PropertyDefinition* MaterialTypeSourceData::FindProperty(AZStd::string_view groupName, AZStd::string_view propertyName) const + bool MaterialTypeSourceData::ApplyPropertyRenames(MaterialPropertyId& propertyId, uint32_t materialTypeVersion) const + { + bool renamed = false; + + for (const VersionUpdateDefinition& versionUpdate : m_versionUpdates) + { + if (materialTypeVersion >= versionUpdate.m_toVersion) + { + continue; + } + + for (const VersionUpdatesRenameOperationDefinition& action : versionUpdate.m_actions) + { + if (action.m_operation == "rename") + { + if (action.m_renameFrom == propertyId.GetFullName().GetStringView()) + { + propertyId = MaterialPropertyId::Parse(action.m_renameTo); + renamed = true; + } + } + else + { + AZ_Warning("Material source data", false, "Unsupported material version update operation '%s'", action.m_operation.c_str()); + } + } + } + + return renamed; + } + + const MaterialTypeSourceData::PropertyDefinition* MaterialTypeSourceData::FindProperty(AZStd::string_view groupName, AZStd::string_view propertyName, uint32_t materialTypeVersion) const { auto groupIter = m_propertyLayout.m_properties.find(groupName); if (groupIter == m_propertyLayout.m_properties.end()) @@ -126,6 +177,27 @@ namespace AZ } } + // Property has not been found, try looking for renames in the version history + + MaterialPropertyId propertyId = MaterialPropertyId{groupName, propertyName}; + ApplyPropertyRenames(propertyId, materialTypeVersion); + + // Do the search again with the new names + + groupIter = m_propertyLayout.m_properties.find(propertyId.GetGroupName().GetStringView()); + if (groupIter == m_propertyLayout.m_properties.end()) + { + return nullptr; + } + + for (const PropertyDefinition& property : groupIter->second) + { + if (property.m_name == propertyId.GetPropertyName().GetStringView()) + { + return &property; + } + } + return nullptr; } @@ -280,6 +352,41 @@ namespace AZ materialTypeAssetCreator.SetElevateWarnings(elevateWarnings); materialTypeAssetCreator.Begin(assetId); + if (m_propertyLayout.m_versionOld != 0) + { + materialTypeAssetCreator.ReportError( + "The field '/propertyLayout/version' is deprecated and moved to '/version'. " + "Please edit this material type source file and move the '\"version\": %u' setting up one level.", + m_propertyLayout.m_versionOld); + return Failure(); + } + + // Set materialtype version and add each version update object into MaterialTypeAsset. + materialTypeAssetCreator.SetVersion(m_version); + { + const AZ::Name rename = AZ::Name{ "rename" }; + + for (const auto& versionUpdate : m_versionUpdates) + { + MaterialVersionUpdate materialVersionUpdate{versionUpdate.m_toVersion}; + for (const auto& action : versionUpdate.m_actions) + { + if (action.m_operation == rename.GetStringView()) + { + materialVersionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction{ + AZ::Name{ action.m_renameFrom }, + AZ::Name{ action.m_renameTo } + }); + } + else + { + materialTypeAssetCreator.ReportWarning("Unsupported material version update operation '%s'", action.m_operation.c_str()); + } + } + materialTypeAssetCreator.AddVersionUpdate(materialVersionUpdate); + } + } + // Used to gather all the UV streams used in this material type from its shaders in alphabetical order. auto semanticComp = [](const RHI::ShaderSemantic& lhs, const RHI::ShaderSemantic& rhs) -> bool { diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassAttachment.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassAttachment.cpp index e0831381e7..3b4f10a968 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassAttachment.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassAttachment.cpp @@ -187,6 +187,7 @@ namespace AZ { m_name = slot.m_name; m_shaderInputName = slot.m_shaderInputName; + m_shaderImageDimensionsNameIndex = slot.m_shaderImageDimensionsName; m_shaderInputArrayIndex = slot.m_shaderInputArrayIndex; m_slotType = slot.m_slotType; m_scopeAttachmentUsage = slot.m_scopeAttachmentUsage; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp index 318ea7d11f..b8115dff10 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -272,15 +273,8 @@ namespace AZ } } - void RenderPass::BindAttachment(const RHI::FrameGraphCompileContext& context, const PassAttachmentBinding& binding, int16_t& imageIndex, int16_t& bufferIndex) + void RenderPass::BindAttachment(const RHI::FrameGraphCompileContext& context, PassAttachmentBinding& binding, int16_t& imageIndex, int16_t& bufferIndex) { - if (binding.m_shaderInputIndex == PassAttachmentBinding::ShaderInputNoBind || - binding.m_scopeAttachmentUsage == RHI::ScopeAttachmentUsage::RenderTarget || - binding.m_scopeAttachmentUsage == RHI::ScopeAttachmentUsage::DepthStencil) - { - return; - } - PassAttachment* attachment = binding.m_attachment.get(); if (attachment) { @@ -293,11 +287,40 @@ namespace AZ inputIndex = imageIndex; } const RHI::ImageView* imageView = context.GetImageView(attachment->GetAttachmentId(), binding.m_attachmentUsageIndex); - m_shaderResourceGroup->SetImageView(RHI::ShaderInputImageIndex(inputIndex), imageView, arrayIndex); - ++imageIndex; + + if (binding.m_shaderImageDimensionsNameIndex.HasName()) + { + RHI::Size size = attachment->m_descriptor.m_image.m_size; + + AZ::Vector4 imageDimensions; + imageDimensions.SetX(float(size.m_width)); + imageDimensions.SetY(float(size.m_height)); + imageDimensions.SetZ(1.0f / float(size.m_width)); + imageDimensions.SetW(1.0f / float(size.m_height)); + + [[maybe_unused]] + bool success = m_shaderResourceGroup->SetConstant(binding.m_shaderImageDimensionsNameIndex, imageDimensions); + AZ_Assert(success, "Pass [%s] Could not find float4 constant [%s] in Shader Resource Group [%s]", + GetPathName().GetCStr(), + binding.m_shaderImageDimensionsNameIndex.GetNameForDebug().GetCStr(), + m_shaderResourceGroup->GetDatabaseName()); + } + + if (binding.m_shaderInputIndex != PassAttachmentBinding::ShaderInputNoBind && + binding.m_scopeAttachmentUsage != RHI::ScopeAttachmentUsage::RenderTarget && + binding.m_scopeAttachmentUsage != RHI::ScopeAttachmentUsage::DepthStencil) + { + m_shaderResourceGroup->SetImageView(RHI::ShaderInputImageIndex(inputIndex), imageView, arrayIndex); + ++imageIndex; + } } else if (attachment->GetAttachmentType() == RHI::AttachmentType::Buffer) { + if (binding.m_shaderInputIndex == PassAttachmentBinding::ShaderInputNoBind) + { + return; + } + if (inputIndex == PassAttachmentBinding::ShaderInputAutoBind) { inputIndex = bufferIndex; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp index 5c7af601fb..e9d8a42641 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -32,8 +33,9 @@ namespace AZ if (auto* serializeContext = azrtti_cast(context)) { serializeContext->Class() - ->Version(10) + ->Version(11) // Material version update ->Field("materialTypeAsset", &MaterialAsset::m_materialTypeAsset) + ->Field("materialTypeVersion", &MaterialAsset::m_materialTypeVersion) ->Field("propertyValues", &MaterialAsset::m_propertyValues) ->Field("propertyNames", &MaterialAsset::m_propertyNames) ; @@ -103,9 +105,25 @@ namespace AZ AZStd::array_view MaterialAsset::GetPropertyValues() const { - if (!m_propertyNames.empty() && m_isDirty) + // If property names are included, they are used to re-arrange the property value list to align with the + // MaterialPropertiesLayout. This realignment would be necessary if the material type is updated with + // a new property layout, and a corresponding material is not reprocessed by the AP and continues using the + // old property layout. + if (!m_propertyNames.empty()) { - const_cast(this)->RealignPropertyValuesAndNames(); + const uint32_t materialTypeVersion = m_materialTypeAsset->GetVersion(); + if (m_materialTypeVersion < materialTypeVersion) + { + // It is possible that the material type has had some properties renamed. If that's the case, and this material + // is still referencing the old property layout, we need to apply any auto updates to rename those properties + // before using them to realign the property values. + const_cast(this)->ApplyVersionUpdates(); + } + + if (m_isDirty) + { + const_cast(this)->RealignPropertyValuesAndNames(); + } } return m_propertyValues; @@ -183,6 +201,40 @@ namespace AZ m_isDirty = false; } + void MaterialAsset::ApplyVersionUpdates() + { + if (m_materialTypeVersion == m_materialTypeAsset->GetVersion()) + { + return; + } + + const uint32_t originalVersion = m_materialTypeVersion; + + bool changesWereApplied = false; + + for (const MaterialVersionUpdate& versionUpdate : m_materialTypeAsset->GetMaterialVersionUpdateList()) + { + if (m_materialTypeVersion < versionUpdate.GetVersion()) + { + if (versionUpdate.ApplyVersionUpdates(*this)) + { + changesWereApplied = true; + m_materialTypeVersion = versionUpdate.GetVersion(); + } + } + } + + if (changesWereApplied) + { + AZ_Warning("MaterialAsset", false, + "This material is based on version '%u' of %s, but the material type is now at version '%u'. " + "Automatic updates are available. Consider updating the .material source file.", + originalVersion, m_materialTypeAsset.ToString().c_str(), m_materialTypeAsset->GetVersion()); + } + + m_materialTypeVersion = m_materialTypeAsset->GetVersion(); + } + void MaterialAsset::ReinitializeMaterialTypeAsset(Data::Asset asset) { Data::Asset newMaterialTypeAsset = { asset.GetAs(), AZ::Data::AssetLoadBehavior::PreLoad }; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAssetCreator.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAssetCreator.cpp index 79d19c15a2..b62a91a98d 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAssetCreator.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAssetCreator.cpp @@ -23,6 +23,7 @@ namespace AZ if (ValidateIsReady()) { m_asset->m_materialTypeAsset = parentMaterial.m_materialTypeAsset; + m_asset->m_materialTypeVersion = m_asset->m_materialTypeAsset->GetVersion(); if (!m_asset->m_materialTypeAsset) { @@ -69,6 +70,7 @@ namespace AZ ReportError("MaterialTypeAsset is null"); return; } + m_asset->m_materialTypeVersion = m_asset->m_materialTypeAsset->GetVersion(); m_materialPropertiesLayout = m_asset->GetMaterialPropertiesLayout(); if (includeMaterialPropertyNames) diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAsset.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAsset.cpp index 913c3206fb..522ba74119 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAsset.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAsset.cpp @@ -37,6 +37,7 @@ namespace AZ void MaterialTypeAsset::Reflect(ReflectContext* context) { + MaterialVersionUpdate::Reflect(context); UvNamePair::Reflect(context); if (auto* serializeContext = azrtti_cast(context)) @@ -44,7 +45,9 @@ namespace AZ serializeContext->RegisterGenericType(); serializeContext->Class() - ->Version(4) // ATOM-15472 + ->Version(5) // Material version update + ->Field("Version", &MaterialTypeAsset::m_version) + ->Field("VersionUpdates", &MaterialTypeAsset::m_materialVersionUpdates) ->Field("ShaderCollection", &MaterialTypeAsset::m_shaderCollection) ->Field("MaterialFunctors", &MaterialTypeAsset::m_materialFunctors) ->Field("MaterialSrgShaderIndex", &MaterialTypeAsset::m_materialSrgShaderIndex) @@ -161,6 +164,11 @@ namespace AZ return m_uvNameMap; } + uint32_t MaterialTypeAsset::GetVersion() const + { + return m_version; + } + void MaterialTypeAsset::SetReady() { m_status = AssetStatus::Ready; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAssetCreator.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAssetCreator.cpp index 4405e835d6..46086dfecc 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAssetCreator.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAssetCreator.cpp @@ -38,7 +38,7 @@ namespace AZ bool MaterialTypeAssetCreator::End(Data::Asset& result) { - if (!ValidateIsReady() || !ValidateEndMaterialProperty()) + if (!ValidateIsReady() || !ValidateEndMaterialProperty() || !ValidateMaterialVersion()) { return false; } @@ -100,6 +100,48 @@ namespace AZ } } + bool MaterialTypeAssetCreator::ValidateMaterialVersion() + { + if (m_asset->m_materialVersionUpdates.empty()) + { + return true; + } + + uint32_t prevVersion = 0; + for(const MaterialVersionUpdate& versionUpdate : m_asset->m_materialVersionUpdates) + { + if (versionUpdate.GetVersion() <= prevVersion) + { + ReportError("Version updates are not sequential. See version update '%u'.", versionUpdate.GetVersion()); + return false; + } + + if (versionUpdate.GetVersion() > m_asset->m_version) + { + ReportError("Version updates go beyond the current material type version. See version update '%u'.", versionUpdate.GetVersion()); + return false; + } + + prevVersion = versionUpdate.GetVersion(); + } + + const auto& lastMaterialVersionUpdate = m_asset->m_materialVersionUpdates.back(); + for (const auto& action : lastMaterialVersionUpdate.GetActions()) + { + const auto propertyIndex = m_asset->m_materialPropertiesLayout->FindPropertyIndex(AZ::Name{ action.m_toPropertyId }); + if (!propertyIndex.IsValid()) + { + ReportError("Renamed property '%s' not found in material property layout. Check that the property name has been " + "upgraded to the correct version", + action.m_toPropertyId.GetCStr()); + return false; + } + + } + + return true; + } + void MaterialTypeAssetCreator::AddShader(const AZ::Data::Asset& shaderAsset, const ShaderVariantId& shaderVaraintId, const AZ::Name& shaderTag) { if (ValidateIsReady() && ValidateNotNull(shaderAsset, "ShaderAsset")) @@ -123,6 +165,16 @@ namespace AZ AddShader(shaderAsset, ShaderVariantId{}, shaderTag); } + void MaterialTypeAssetCreator::SetVersion(uint32_t version) + { + m_asset->m_version = version; + } + + void MaterialTypeAssetCreator::AddVersionUpdate(const MaterialVersionUpdate& materialVersionUpdate) + { + m_asset->m_materialVersionUpdates.push_back(materialVersionUpdate); + } + void MaterialTypeAssetCreator::ClaimShaderOptionOwnership(const Name& shaderOptionName) { bool optionFound = false; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialVersionUpdate.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialVersionUpdate.cpp new file mode 100644 index 0000000000..b387e502e2 --- /dev/null +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialVersionUpdate.cpp @@ -0,0 +1,89 @@ +/* + * 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 AZ +{ + namespace RPI + { + void MaterialVersionUpdate::RenamePropertyAction::Reflect(ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("From", &RenamePropertyAction::m_fromPropertyId) + ->Field("To", &RenamePropertyAction::m_toPropertyId) + ; + } + } + + void MaterialVersionUpdate::Reflect(ReflectContext* context) + { + MaterialVersionUpdate::RenamePropertyAction::Reflect(context); + + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->RegisterGenericType(); + + serializeContext->Class() + ->Version(1) + ->Field("ToVersion", &MaterialVersionUpdate::m_toVersion) + ->Field("Actions", &MaterialVersionUpdate::m_actions) + ; + } + } + + MaterialVersionUpdate::MaterialVersionUpdate(uint32_t toVersion) + : m_toVersion(toVersion) + { + } + + uint32_t MaterialVersionUpdate::GetVersion() const + { + return m_toVersion; + } + + void MaterialVersionUpdate::SetVersion(uint32_t toVersion) + { + m_toVersion = toVersion; + } + + bool MaterialVersionUpdate::ApplyVersionUpdates(MaterialAsset& materialAsset) const + { + bool changesWereApplied = false; + + for (auto& propertyName : materialAsset.m_propertyNames) + { + for (const auto& action : m_actions) + { + if (propertyName == action.m_fromPropertyId) + { + propertyName = action.m_toPropertyId; + changesWereApplied = true; + } + } + } + + return changesWereApplied; + } + + const AZ::RPI::MaterialVersionUpdate::Actions& MaterialVersionUpdate::GetActions() const + { + return m_actions; + } + + void MaterialVersionUpdate::AddAction(const RenamePropertyAction& action) + { + m_actions.push_back(action); + } + } // namespace RPI +} // namespace AZ diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Pass/PassAttachmentReflect.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Pass/PassAttachmentReflect.cpp index 39be08d299..602793ba19 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Pass/PassAttachmentReflect.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Pass/PassAttachmentReflect.cpp @@ -56,9 +56,10 @@ namespace AZ ; serializeContext->Class() - ->Version(1) + ->Version(2) ->Field("Name", &PassSlot::m_name) ->Field("ShaderInputName", &PassSlot::m_shaderInputName) + ->Field("ShaderImageDimensionsConstant", &PassSlot::m_shaderImageDimensionsName) ->Field("ShaderInputArrayIndex", &PassSlot::m_shaderInputArrayIndex) ->Field("SlotType", &PassSlot::m_slotType) ->Field("ScopeAttachmentUsage", &PassSlot::m_scopeAttachmentUsage) diff --git a/Gems/Atom/RPI/Code/Tests/Common/AssetSystemStub.cpp b/Gems/Atom/RPI/Code/Tests/Common/AssetSystemStub.cpp index a5511edea1..31a46e9a6f 100644 --- a/Gems/Atom/RPI/Code/Tests/Common/AssetSystemStub.cpp +++ b/Gems/Atom/RPI/Code/Tests/Common/AssetSystemStub.cpp @@ -7,6 +7,7 @@ */ #include +#include namespace UnitTest { @@ -36,12 +37,17 @@ namespace UnitTest // Because GetSourceInfoBySourcePath should always return 0 for the sub-id, since it's about the source file not product file. sourceInfo.m_assetInfo.m_assetId.m_subId = 0; - m_sourceInfoMap.emplace(sourcePath, sourceInfo); + AZStd::string normalizedSourcePath = sourcePath; + AzFramework::StringFunc::Path::Normalize(normalizedSourcePath); + m_sourceInfoMap.emplace(normalizedSourcePath, sourceInfo); } bool AssetSystemStub::GetSourceInfoBySourcePath(const char* sourcePath, AZ::Data::AssetInfo& assetInfo, AZStd::string& watchFolder) { - auto iter = m_sourceInfoMap.find(sourcePath); + AZStd::string normalizedSourcePath = sourcePath; + AzFramework::StringFunc::Path::Normalize(normalizedSourcePath); + + auto iter = m_sourceInfoMap.find(normalizedSourcePath); if (iter != m_sourceInfoMap.end()) { diff --git a/Gems/Atom/RPI/Code/Tests/Material/MaterialAssetTests.cpp b/Gems/Atom/RPI/Code/Tests/Material/MaterialAssetTests.cpp index ce223ceb35..58a852f176 100644 --- a/Gems/Atom/RPI/Code/Tests/Material/MaterialAssetTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Material/MaterialAssetTests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -63,6 +64,11 @@ namespace UnitTest RPITestFixture::TearDown(); } + + void ReplaceMaterialType(Data::Asset materialAsset, Data::Asset upgradedMaterialTypeAsset) + { + materialAsset->m_materialTypeAsset = upgradedMaterialTypeAsset; + } }; TEST_F(MaterialAssetTests, Basic) @@ -202,6 +208,81 @@ namespace UnitTest EXPECT_EQ(serializedAsset->GetPropertyValues()[8].GetValue>(), streamingImageAsset); } + TEST_F(MaterialAssetTests, UpgradeMaterialAsset) + { + // Here we test the main way that a material asset upgrade would be applied at runtime: A material type is updated to + // both rename a property *and* change the order in which properties appear in the layout. In this case, the new name + // must be identified and then that new name is used to find the appropriate index in the property layout. + + auto materialSrgLayout = CreateCommonTestMaterialSrgLayout(); + + auto shaderAsset = CreateTestShaderAsset(Uuid::CreateRandom(), materialSrgLayout); + + Data::Asset testMaterialTypeAssetV1; + MaterialTypeAssetCreator materialTypeCreator; + materialTypeCreator.Begin(Uuid::CreateRandom()); + materialTypeCreator.AddShader(shaderAsset); + AddMaterialPropertyForSrg(materialTypeCreator, Name{ "MyInt" }, MaterialPropertyDataType::Int, Name{ "m_int" }); + AddMaterialPropertyForSrg(materialTypeCreator, Name{ "MyUInt" }, MaterialPropertyDataType::UInt, Name{ "m_uint" }); + AddMaterialPropertyForSrg(materialTypeCreator, Name{ "MyFloat" }, MaterialPropertyDataType::Float, Name{ "m_float" }); + EXPECT_TRUE(materialTypeCreator.End(testMaterialTypeAssetV1)); + + // Construct the material asset with materialTypeAsset version 1 + Data::AssetId assetId(Uuid::CreateRandom()); + + MaterialAssetCreator creator; + const bool includePropertyNames = true; + creator.Begin(assetId, *testMaterialTypeAssetV1, includePropertyNames); + creator.SetPropertyValue(Name{ "MyInt" }, 7); + creator.SetPropertyValue(Name{ "MyUInt" }, 8u); + creator.SetPropertyValue(Name{ "MyFloat" }, 9.0f); + Data::Asset materialAsset; + EXPECT_TRUE(creator.End(materialAsset)); + + // Prepare material type asset version 2 with the update actions + MaterialVersionUpdate versionUpdate(2); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction( + { + Name{ "MyInt" }, + Name{ "MyIntRenamed" } + })); + + Data::Asset testMaterialTypeAssetV2; + materialTypeCreator.Begin(Uuid::CreateRandom()); + materialTypeCreator.SetVersion(versionUpdate.GetVersion()); + materialTypeCreator.AddVersionUpdate(versionUpdate); + materialTypeCreator.AddShader(shaderAsset); + // Now we add the properties in a different order from before, and use the new name for MyInt. + AddMaterialPropertyForSrg(materialTypeCreator, Name{ "MyUInt" }, MaterialPropertyDataType::UInt, Name{ "m_uint" }); + AddMaterialPropertyForSrg(materialTypeCreator, Name{ "MyFloat" }, MaterialPropertyDataType::Float, Name{ "m_float" }); + AddMaterialPropertyForSrg(materialTypeCreator, Name{ "MyIntRenamed" }, MaterialPropertyDataType::Int, Name{ "m_int" }); + EXPECT_TRUE(materialTypeCreator.End(testMaterialTypeAssetV2)); + + // This is our way of faking the idea that an old version of the MaterialAsset could be loaded with a new version of the MaterialTypeAsset. + ReplaceMaterialType(materialAsset, testMaterialTypeAssetV2); + + // This can find errors and warnings, we are looking for a warning when the version update is applied + ErrorMessageFinder warningFinder; + warningFinder.AddExpectedErrorMessage("Automatic updates are available. Consider updating the .material source file"); + warningFinder.AddExpectedErrorMessage("This material is based on version '1'"); + warningFinder.AddExpectedErrorMessage("material type is now at version '2'"); + + // Even though this material was created using the old version of the material type, it's property values should get automatically + // updated to align with the new property layout in the latest MaterialTypeAsset. + MaterialPropertyIndex myIntIndex = materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(Name{"MyIntRenamed"}); + EXPECT_EQ(2, myIntIndex.GetIndex()); + EXPECT_EQ(7, materialAsset->GetPropertyValues()[myIntIndex.GetIndex()].GetValue()); + + warningFinder.CheckExpectedErrorsFound(); + + // Since the MaterialAsset has already been updated, and the warning reported once, we should not see the "consider updating" + // warning reported again on subsequent property accesses. + warningFinder.Reset(); + myIntIndex = materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(Name{"MyIntRenamed"}); + EXPECT_EQ(2, myIntIndex.GetIndex()); + EXPECT_EQ(7, materialAsset->GetPropertyValues()[myIntIndex.GetIndex()].GetValue()); + } + TEST_F(MaterialAssetTests, Error_NoBegin) { Data::AssetId assetId(Uuid::CreateRandom()); diff --git a/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp b/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp index dd3b3b2711..acce52ae8e 100644 --- a/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include #include #include @@ -60,22 +62,72 @@ namespace UnitTest localFileIO->SetAlias("@exefolder@", rootPath); m_testMaterialSrgLayout = CreateCommonTestMaterialSrgLayout(); - m_testShaderAsset = CreateTestShaderAsset(Uuid::CreateRandom(), m_testMaterialSrgLayout); + m_assetSystemStub.RegisterSourceInfo("@exefolder@/Temp/test.shader", m_testShaderAsset.GetId()); + + // The MaterialSourceData relies on both MaterialTypeSourceData and MaterialTypeAsset. We have to make sure the + // .materialtype file is present on disk, and that the MaterialTypeAsset is available through the asset database stub... + + const char* materialTypeJson = R"( + { + "version": 10, + "propertyLayout": { + "properties": { + "general": [ + {"name": "MyBool", "type": "bool"}, + {"name": "MyInt", "type": "Int"}, + {"name": "MyUInt", "type": "UInt"}, + {"name": "MyFloat", "type": "Float"}, + {"name": "MyFloat2", "type": "Vector2"}, + {"name": "MyFloat3", "type": "Vector3"}, + {"name": "MyFloat4", "type": "Vector4"}, + {"name": "MyColor", "type": "Color"}, + {"name": "MyImage", "type": "Image"}, + {"name": "MyEnum", "type": "Enum", "enumValues": ["Enum0", "Enum1", "Enum2"], "defaultValue": "Enum0"} + ] + } + }, + "shaders": [ + { + "file": "@exefolder@/Temp/test.shader" + } + ], + "versionUpdates": [ + { + "toVersion": 2, + "actions": [ + {"op": "rename", "from": "general.testColorNameA", "to": "general.testColorNameB"} + ] + }, + { + "toVersion": 4, + "actions": [ + {"op": "rename", "from": "general.testColorNameB", "to": "general.testColorNameC"} + ] + }, + { + "toVersion": 10, + "actions": [ + {"op": "rename", "from": "general.testColorNameC", "to": "general.MyColor"} + ] + } + ] + } + )"; + + AZ::Utils::WriteFile(materialTypeJson, "@exefolder@/Temp/test.materialtype"); - MaterialTypeAssetCreator materialTypeCreator; - materialTypeCreator.Begin(Uuid::CreateRandom()); - materialTypeCreator.AddShader(m_testShaderAsset); - AddCommonTestMaterialProperties(materialTypeCreator, "general."); - materialTypeCreator.End(m_testMaterialTypeAsset); + MaterialTypeSourceData materialTypeSourceData; + LoadTestDataFromJson(materialTypeSourceData, materialTypeJson); + m_testMaterialTypeAsset = materialTypeSourceData.CreateMaterialTypeAsset(Uuid::CreateRandom()).TakeValue(); // Since this test doesn't actually instantiate a Material, it won't need to instantiate this ImageAsset, so all we // need is an asset reference with a valid ID. m_testImageAsset = Data::Asset{ Data::AssetId{Uuid::CreateRandom(), StreamingImageAsset::GetImageAssetSubId()}, azrtti_typeid() }; // Register the test assets with the AssetSystemStub so CreateMaterialAsset() can use AssetUtils. - m_assetSystemStub.RegisterSourceInfo("test.materialtype", m_testMaterialTypeAsset.GetId()); - m_assetSystemStub.RegisterSourceInfo("test.streamingimage", m_testImageAsset.GetId()); + m_assetSystemStub.RegisterSourceInfo("@exefolder@/Temp/test.materialtype", m_testMaterialTypeAsset.GetId()); + m_assetSystemStub.RegisterSourceInfo("@exefolder@/Temp/test.streamingimage", m_testImageAsset.GetId()); } void TearDown() override @@ -88,12 +140,12 @@ namespace UnitTest RPITestFixture::TearDown(); } }; - + void AddPropertyGroup(MaterialSourceData& material, AZStd::string_view groupName) { material.m_properties.insert(groupName); } - + void AddProperty(MaterialSourceData& material, AZStd::string_view groupName, AZStd::string_view propertyName, const MaterialPropertyValue& anyValue) { material.m_properties[groupName][propertyName].m_value = anyValue; @@ -103,7 +155,7 @@ namespace UnitTest { MaterialSourceData sourceData; - sourceData.m_materialType = "test.materialtype"; + sourceData.m_materialType = "@exefolder@/Temp/test.materialtype"; AddPropertyGroup(sourceData, "general"); AddProperty(sourceData, "general", "MyBool", true); AddProperty(sourceData, "general", "MyInt", -10); @@ -113,7 +165,7 @@ namespace UnitTest AddProperty(sourceData, "general", "MyFloat2", AZ::Vector2(2.1f, 2.2f)); AddProperty(sourceData, "general", "MyFloat3", AZ::Vector3(3.1f, 3.2f, 3.3f)); AddProperty(sourceData, "general", "MyFloat4", AZ::Vector4(4.1f, 4.2f, 4.3f, 4.4f)); - AddProperty(sourceData, "general", "MyImage", AZStd::string("test.streamingimage")); + AddProperty(sourceData, "general", "MyImage", AZStd::string("@exefolder@/Temp/test.streamingimage")); AddProperty(sourceData, "general", "MyEnum", AZStd::string("Enum1")); auto materialAssetOutcome = sourceData.CreateMaterialAsset(Uuid::CreateRandom(), "", true); @@ -139,7 +191,7 @@ namespace UnitTest EXPECT_STREQ(a.m_materialType.data(), b.m_materialType.data()); EXPECT_STREQ(a.m_description.data(), b.m_description.data()); EXPECT_STREQ(a.m_parentMaterial.data(), b.m_parentMaterial.data()); - EXPECT_EQ(a.m_propertyLayoutVersion, b.m_propertyLayoutVersion); + EXPECT_EQ(a.m_materialTypeVersion, b.m_materialTypeVersion); EXPECT_EQ(a.m_properties.size(), b.m_properties.size()); for (auto& groupA : a.m_properties) @@ -170,7 +222,7 @@ namespace UnitTest auto& propertyA = propertyIterA.second; auto& propertyB = propertyIterB->second; - + bool typesMatch = propertyA.m_value.GetTypeId() == propertyB.m_value.GetTypeId(); EXPECT_TRUE(typesMatch); if (typesMatch) @@ -229,8 +281,8 @@ namespace UnitTest " } \n" "} \n"; - const char* materialTypeFilePath = "@exefolder@/Gems/Atom/RPI/Code/Tests/Material/Temp/roundTripTest.materialtype"; - + const char* materialTypeFilePath = "@exefolder@/Temp/roundTripTest.materialtype"; + AZ::IO::FileIOStream file; EXPECT_TRUE(file.Open(materialTypeFilePath, AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath)); file.Write(strlen(materialTypeJson), materialTypeJson); @@ -240,7 +292,7 @@ namespace UnitTest sourceDataOriginal.m_materialType = materialTypeFilePath; sourceDataOriginal.m_parentMaterial = materialTypeFilePath; sourceDataOriginal.m_description = "This is a description"; - sourceDataOriginal.m_propertyLayoutVersion = 7; + sourceDataOriginal.m_materialTypeVersion = 7; AddPropertyGroup(sourceDataOriginal, "groupA"); AddProperty(sourceDataOriginal, "groupA", "MyBool", true); AddProperty(sourceDataOriginal, "groupA", "MyInt", -10); @@ -252,14 +304,14 @@ namespace UnitTest AddPropertyGroup(sourceDataOriginal, "groupC"); AddProperty(sourceDataOriginal, "groupC", "MyFloat4", AZ::Vector4(4.1f, 4.2f, 4.3f, 4.4f)); AddProperty(sourceDataOriginal, "groupC", "MyColor", AZ::Color{0.1f, 0.2f, 0.3f, 0.4f}); - AddProperty(sourceDataOriginal, "groupC", "MyImage", AZStd::string("test.streamingimage")); + AddProperty(sourceDataOriginal, "groupC", "MyImage", AZStd::string("@exefolder@/Temp/test.streamingimage")); AZStd::string sourceDataSerialized; JsonTestResult storeResult = StoreTestDataToJson(sourceDataOriginal, sourceDataSerialized); MaterialSourceData sourceDataCopy; JsonTestResult loadResult = LoadTestDataFromJson(sourceDataCopy, sourceDataSerialized); - + CheckEqual(sourceDataOriginal, sourceDataCopy); } @@ -277,10 +329,10 @@ namespace UnitTest ] } } - } + } )"; - const char* materialTypeFilePath = "@exefolder@/Gems/Atom/RPI/Code/Tests/Material/Temp/simpleMaterialType.materialtype"; + const char* materialTypeFilePath = "@exefolder@/Temp/simpleMaterialType.materialtype"; AZ::IO::FileIOStream file; EXPECT_TRUE(file.Open(materialTypeFilePath, AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath)); @@ -296,7 +348,7 @@ namespace UnitTest "testColor": [0.1,0.2,0.3] } }, - "materialType": "@exefolder@/Gems/Atom/RPI/Code/Tests/Material/Temp/simpleMaterialType.materialtype" + "materialType": "@exefolder@/Temp/simpleMaterialType.materialtype" } )"; @@ -330,7 +382,7 @@ namespace UnitTest { const AZStd::string inputJson = R"( { - "propertyLayoutVersion": 1, + "materialTypeVersion": 1, "properties": { "baseColor": { "color": [1.0,1.0,1.0] @@ -354,7 +406,7 @@ namespace UnitTest const AZStd::string inputJson = R"( { "materialType": "DoesNotExist.materialtype", - "propertyLayoutVersion": 1, + "materialTypeVersion": 1, "properties": { "baseColor": { "color": [1.0,1.0,1.0] @@ -387,10 +439,10 @@ namespace UnitTest ] } } - } + } )"; - const char* materialTypeFilePath = "@exefolder@/Gems/Atom/RPI/Code/Tests/Material/Temp/simpleMaterialType.materialtype"; + const char* materialTypeFilePath = "@exefolder@/Temp/simpleMaterialType.materialtype"; AZ::IO::FileIOStream file; EXPECT_TRUE(file.Open(materialTypeFilePath, AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath)); @@ -399,8 +451,8 @@ namespace UnitTest const AZStd::string inputJson = R"( { - "materialType": "@exefolder@/Gems/Atom/RPI/Code/Tests/Material/Temp/simpleMaterialType.materialtype", - "propertyLayoutVersion": 1, + "materialType": "@exefolder@/Temp/simpleMaterialType.materialtype", + "materialTypeVersion": 1, "properties": { "general": { "testColor": [1.0,1.0,1.0] @@ -433,10 +485,10 @@ namespace UnitTest ] } } - } + } )"; - const char* materialTypeFilePath = "@exefolder@/Gems/Atom/RPI/Code/Tests/Material/Temp/simpleMaterialType.materialtype"; + const char* materialTypeFilePath = "@exefolder@/Temp/simpleMaterialType.materialtype"; AZ::IO::FileIOStream file; EXPECT_TRUE(file.Open(materialTypeFilePath, AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath)); @@ -445,8 +497,8 @@ namespace UnitTest const AZStd::string inputJson = R"( { - "materialType": "@exefolder@/Gems/Atom/RPI/Code/Tests/Material/Temp/simpleMaterialType.materialtype", - "propertyLayoutVersion": 1, + "materialType": "@exefolder@/Temp/simpleMaterialType.materialtype", + "materialTypeVersion": 1, "properties": { "general": { "doesNotExist": [1.0,1.0,1.0] @@ -467,20 +519,20 @@ namespace UnitTest TEST_F(MaterialSourceDataTests, CreateMaterialAsset_MultiLevelDataInheritance) { MaterialSourceData sourceDataLevel1; - sourceDataLevel1.m_materialType = "test.materialtype"; + sourceDataLevel1.m_materialType = "@exefolder@/Temp/test.materialtype"; AddPropertyGroup(sourceDataLevel1, "general"); AddProperty(sourceDataLevel1, "general", "MyFloat", 1.5f); AddProperty(sourceDataLevel1, "general", "MyColor", AZ::Color{0.1f, 0.2f, 0.3f, 0.4f}); MaterialSourceData sourceDataLevel2; - sourceDataLevel2.m_materialType = "test.materialtype"; + sourceDataLevel2.m_materialType = "@exefolder@/Temp/test.materialtype"; sourceDataLevel2.m_parentMaterial = "level1.material"; AddPropertyGroup(sourceDataLevel2, "general"); AddProperty(sourceDataLevel2, "general", "MyColor", AZ::Color{0.15f, 0.25f, 0.35f, 0.45f}); AddProperty(sourceDataLevel2, "general", "MyFloat2", AZ::Vector2{4.1f, 4.2f}); MaterialSourceData sourceDataLevel3; - sourceDataLevel3.m_materialType = "test.materialtype"; + sourceDataLevel3.m_materialType = "@exefolder@/Temp/test.materialtype"; sourceDataLevel3.m_parentMaterial = "level2.material"; AddPropertyGroup(sourceDataLevel3, "general"); AddProperty(sourceDataLevel3, "general", "MyFloat", 3.5f); @@ -497,7 +549,7 @@ namespace UnitTest auto materialAssetLevel3 = sourceDataLevel3.CreateMaterialAsset(Uuid::CreateRandom(), "", true); EXPECT_TRUE(materialAssetLevel3.IsSuccess()); - + auto layout = m_testMaterialTypeAsset->GetMaterialPropertiesLayout(); MaterialPropertyIndex myFloat = layout->FindPropertyIndex(Name("general.MyFloat")); MaterialPropertyIndex myFloat2 = layout->FindPropertyIndex(Name("general.MyFloat2")); @@ -535,14 +587,14 @@ namespace UnitTest m_assetSystemStub.RegisterSourceInfo("otherBase.materialtype", otherMaterialType.GetId()); MaterialSourceData sourceDataLevel1; - sourceDataLevel1.m_materialType = "test.materialtype"; + sourceDataLevel1.m_materialType = "@exefolder@/Temp/test.materialtype"; MaterialSourceData sourceDataLevel2; - sourceDataLevel2.m_materialType = "test.materialtype"; + sourceDataLevel2.m_materialType = "@exefolder@/Temp/test.materialtype"; sourceDataLevel2.m_parentMaterial = "level1.material"; MaterialSourceData sourceDataLevel3; - sourceDataLevel3.m_materialType = "otherBase.materialtype"; + sourceDataLevel3.m_materialType = "@exefolder@/Temp/otherBase.materialtype"; sourceDataLevel3.m_parentMaterial = "level2.material"; auto materialAssetLevel1 = sourceDataLevel1.CreateMaterialAsset(Uuid::CreateRandom(), "", true); @@ -570,7 +622,7 @@ namespace UnitTest { MaterialSourceData sourceData; - sourceData.m_materialType = "test.materialtype"; + sourceData.m_materialType = "@exefolder@/Temp/test.materialtype"; AddPropertyGroup(sourceData, "general"); @@ -587,7 +639,7 @@ namespace UnitTest { MaterialSourceData sourceData; - sourceData.m_materialType = "test.materialtype"; + sourceData.m_materialType = "@exefolder@/Temp/test.materialtype"; AddPropertyGroup(sourceData, "general"); @@ -629,7 +681,7 @@ namespace UnitTest expectWarning([](MaterialSourceData& materialSourceData) { - AddProperty(materialSourceData, "general", "DoesNotExist", AZStd::string("test.streamingimage")); + AddProperty(materialSourceData, "general", "DoesNotExist", AZStd::string("@exefolder@/Temp/test.streamingimage")); }); // Missing image reference @@ -638,6 +690,124 @@ namespace UnitTest AddProperty(materialSourceData, "general", "MyImage", AZStd::string("doesNotExist.streamingimage")); }, 3); // Expect a 3rd error because AssetUtils reports its own assertion failure } + + + TEST_F(MaterialSourceDataTests, Load_MaterialTypeVersionUpdate) + { + const AZStd::string inputJson = R"( + { + "materialType": "@exefolder@/Temp/test.materialtype", + "materialTypeVersion": 1, + "properties": { + "general": { + "testColorNameA": [0.1, 0.2, 0.3] + } + } + } + )"; + + MaterialSourceData material; + JsonTestResult loadResult = LoadTestDataFromJson(material, inputJson); + + EXPECT_EQ(AZ::JsonSerializationResult::Tasks::ReadField, loadResult.m_jsonResultCode.GetTask()); + EXPECT_EQ(AZ::JsonSerializationResult::Processing::Completed, loadResult.m_jsonResultCode.GetProcessing()); + + // Initially, the loaded material data will match the .material file exactly. This gives us the accurate representation of + // what's actually saved on disk. + + EXPECT_NE(material.m_properties["general"].find("testColorNameA"), material.m_properties["general"].end()); + EXPECT_EQ(material.m_properties["general"].find("testColorNameB"), material.m_properties["general"].end()); + EXPECT_EQ(material.m_properties["general"].find("testColorNameC"), material.m_properties["general"].end()); + EXPECT_EQ(material.m_properties["general"].find("MyColor"), material.m_properties["general"].end()); + + AZ::Color testColor = material.m_properties["general"]["testColorNameA"].m_value.GetValue(); + EXPECT_TRUE(AZ::Color(0.1f, 0.2f, 0.3f, 1.0f).IsClose(testColor, 0.01)); + + EXPECT_EQ(1, material.m_materialTypeVersion); + + // Then we force the material data to update to the latest material type version specification + ErrorMessageFinder warningFinder; // Note this finds errors and warnings, and we're looking for a warning. + warningFinder.AddExpectedErrorMessage("Automatic updates are available. Consider updating the .material source file"); + warningFinder.AddExpectedErrorMessage("This material is based on version '1'"); + warningFinder.AddExpectedErrorMessage("material type is now at version '10'"); + material.ApplyVersionUpdates(); + warningFinder.CheckExpectedErrorsFound(); + + // Now the material data should match the latest material type. + // Look for the property under the latest name in the material type, not the name used in the .material file. + + EXPECT_EQ(material.m_properties["general"].find("testColorNameA"), material.m_properties["general"].end()); + EXPECT_EQ(material.m_properties["general"].find("testColorNameB"), material.m_properties["general"].end()); + EXPECT_EQ(material.m_properties["general"].find("testColorNameC"), material.m_properties["general"].end()); + EXPECT_NE(material.m_properties["general"].find("MyColor"), material.m_properties["general"].end()); + + testColor = material.m_properties["general"]["MyColor"].m_value.GetValue(); + EXPECT_TRUE(AZ::Color(0.1f, 0.2f, 0.3f, 1.0f).IsClose(testColor, 0.01)); + + EXPECT_EQ(10, material.m_materialTypeVersion); + + // Calling ApplyVersionUpdates() again should not report the warning again, since the material has already been updated. + warningFinder.Reset(); + material.ApplyVersionUpdates(); + } + + TEST_F(MaterialSourceDataTests, Load_MaterialTypeVersionPartialUpdate) + { + // This case is similar to Load_MaterialTypeVersionUpdate but we start at a later + // version so only some of the version updates are applied. + + const AZStd::string inputJson = R"( + { + "materialType": "@exefolder@/Temp/test.materialtype", + "materialTypeVersion": 3, + "properties": { + "general": { + "testColorNameB": [0.1, 0.2, 0.3] + } + } + } + )"; + + MaterialSourceData material; + JsonTestResult loadResult = LoadTestDataFromJson(material, inputJson); + + EXPECT_EQ(AZ::JsonSerializationResult::Tasks::ReadField, loadResult.m_jsonResultCode.GetTask()); + EXPECT_EQ(AZ::JsonSerializationResult::Processing::Completed, loadResult.m_jsonResultCode.GetProcessing()); + + material.ApplyVersionUpdates(); + + AZ::Color testColor = material.m_properties["general"]["MyColor"].m_value.GetValue(); + EXPECT_TRUE(AZ::Color(0.1f, 0.2f, 0.3f, 1.0f).IsClose(testColor, 0.01)); + + EXPECT_EQ(10, material.m_materialTypeVersion); + } + + TEST_F(MaterialSourceDataTests, Load_Error_MaterialTypeVersionUpdateWithMismatchedVersion) + { + const AZStd::string inputJson = R"( + { + "materialType": "@exefolder@/Temp/test.materialtype", + "materialTypeVersion": 3, // At this version, the property should be testColorNameB not testColorNameA + "properties": { + "general": { + "testColorNameA": [0.1, 0.2, 0.3] + } + } + } + )"; + + MaterialSourceData material; + JsonTestResult loadResult = LoadTestDataFromJson(material, inputJson); + + loadResult.ContainsMessage("/properties/general/testColorNameA", "Property 'general.testColorNameA' not found in material type."); + + EXPECT_FALSE(material.m_properties["general"]["testColorNameA"].m_value.IsValid()); + + material.ApplyVersionUpdates(); + + EXPECT_FALSE(material.m_properties["general"]["MyColor"].m_value.IsValid()); + } + } diff --git a/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeAssetTests.cpp b/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeAssetTests.cpp index b9774c84d9..81b723ec04 100644 --- a/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeAssetTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeAssetTests.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -153,6 +154,16 @@ namespace UnitTest MaterialTypeAssetCreator materialTypeCreator; materialTypeCreator.Begin(assetId); + // Version updates + MaterialVersionUpdate versionUpdate(2); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction( + { + Name{ "EnableSpecialPassPrev" }, + Name{ "EnableSpecialPass" } + })); + materialTypeCreator.SetVersion(versionUpdate.GetVersion()); + materialTypeCreator.AddVersionUpdate(versionUpdate); + // Built-in shader materialTypeCreator.AddShader(m_testShaderAsset); @@ -198,7 +209,7 @@ namespace UnitTest { EXPECT_EQ(m_testMaterialSrgLayout, materialTypeAsset->GetMaterialSrgLayout()); EXPECT_EQ(5, materialTypeAsset->GetMaterialPropertiesLayout()->GetPropertyCount()); - + EXPECT_EQ(2, materialTypeAsset->GetVersion()); // Check aliased properties const MaterialPropertyIndex colorIndex = materialTypeAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(Name{ "MyColor" }); @@ -490,6 +501,106 @@ namespace UnitTest }); } + TEST_F(MaterialTypeAssetTests, Error_InvalidMaterialVersionUpdate_WrongName) + { + Data::Asset materialTypeAsset; + + Data::AssetId assetId(Uuid::CreateRandom()); + + MaterialTypeAssetCreator materialTypeCreator; + materialTypeCreator.Begin(assetId); + + // Invalid version updates + MaterialVersionUpdate versionUpdate(2); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction( + { + Name{ "EnableSpecialPassPrev" }, + Name{ "InvalidPropertyName" } + })); + materialTypeCreator.SetVersion(versionUpdate.GetVersion()); + materialTypeCreator.AddVersionUpdate(versionUpdate); + materialTypeCreator.AddShader(m_testShaderAsset); + + materialTypeCreator.BeginMaterialProperty(Name{ "EnableSpecialPass" }, MaterialPropertyDataType::Bool); + materialTypeCreator.EndMaterialProperty(); + + AZ_TEST_START_ASSERTTEST; + EXPECT_FALSE(materialTypeCreator.End(materialTypeAsset)); + AZ_TEST_STOP_ASSERTTEST(1); + EXPECT_EQ(1, materialTypeCreator.GetErrorCount()); + } + + TEST_F(MaterialTypeAssetTests, Error_InvalidMaterialVersionUpdate_WrongOrder) + { + MaterialTypeAssetCreator materialTypeCreator; + materialTypeCreator.Begin(Uuid::CreateRandom()); + + materialTypeCreator.SetVersion(4); + materialTypeCreator.AddShader(m_testShaderAsset); + materialTypeCreator.BeginMaterialProperty(Name{ "d" }, MaterialPropertyDataType::Bool); + materialTypeCreator.EndMaterialProperty(); + + ErrorMessageFinder errorMessageFinder; + errorMessageFinder.AddExpectedErrorMessage("Version updates are not sequential. See version update '3'"); + + { + MaterialVersionUpdate versionUpdate(2); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction({Name{ "a" },Name{ "b" }})); + materialTypeCreator.AddVersionUpdate(versionUpdate); + } + + { + MaterialVersionUpdate versionUpdate(4); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction({Name{ "b" },Name{ "c" }})); + materialTypeCreator.AddVersionUpdate(versionUpdate); + } + + { + MaterialVersionUpdate versionUpdate(3); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction({Name{ "c" },Name{ "d" }})); + materialTypeCreator.AddVersionUpdate(versionUpdate); + } + + Data::Asset materialTypeAsset; + EXPECT_FALSE(materialTypeCreator.End(materialTypeAsset)); + + errorMessageFinder.CheckExpectedErrorsFound(); + + EXPECT_EQ(1, materialTypeCreator.GetErrorCount()); + } + + TEST_F(MaterialTypeAssetTests, Error_InvalidMaterialVersionUpdate_GoesTooFar) + { + MaterialTypeAssetCreator materialTypeCreator; + materialTypeCreator.Begin(Uuid::CreateRandom()); + + materialTypeCreator.SetVersion(3); + materialTypeCreator.AddShader(m_testShaderAsset); + materialTypeCreator.BeginMaterialProperty(Name{ "d" }, MaterialPropertyDataType::Bool); + materialTypeCreator.EndMaterialProperty(); + + ErrorMessageFinder errorMessageFinder; + errorMessageFinder.AddExpectedErrorMessage("Version updates go beyond the current material type version. See version update '4'"); + + { + MaterialVersionUpdate versionUpdate(2); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction({Name{ "a" },Name{ "b" }})); + materialTypeCreator.AddVersionUpdate(versionUpdate); + } + + { + MaterialVersionUpdate versionUpdate(4); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction({Name{ "b" },Name{ "c" }})); + materialTypeCreator.AddVersionUpdate(versionUpdate); + } + + Data::Asset materialTypeAsset; + EXPECT_FALSE(materialTypeCreator.End(materialTypeAsset)); + + errorMessageFinder.CheckExpectedErrorsFound(); + + EXPECT_EQ(1, materialTypeCreator.GetErrorCount()); + } TEST_F(MaterialTypeAssetTests, MaterialTypeWithNoSRGOrProperties) { diff --git a/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp b/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp index ba4a58b9ff..b811e630d0 100644 --- a/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -978,8 +979,16 @@ namespace UnitTest const AZStd::string inputJson = R"( { "description": "This is a general description about the material", + "version": 2, + "versionUpdates": [ + { + "toVersion": 2, + "actions": [ + { "op": "rename", "from": "groupA.fooPrev", "to": "groupA.foo" } + ] + } + ], "propertyLayout": { - "version": 2, "groups": [ { "name": "groupA", @@ -1062,7 +1071,12 @@ namespace UnitTest EXPECT_EQ(material.m_description, "This is a general description about the material"); - EXPECT_EQ(material.m_propertyLayout.m_version, 2); + EXPECT_EQ(material.m_version, 2); + EXPECT_EQ(material.m_versionUpdates.size(), 1); + EXPECT_EQ(material.m_versionUpdates[0].m_toVersion, 2); + EXPECT_EQ(material.m_versionUpdates[0].m_actions[0].m_operation, "rename"); + EXPECT_EQ(material.m_versionUpdates[0].m_actions[0].m_renameFrom, "groupA.fooPrev"); + EXPECT_EQ(material.m_versionUpdates[0].m_actions[0].m_renameTo, "groupA.foo"); EXPECT_EQ(material.m_propertyLayout.m_groups.size(), 2); EXPECT_TRUE(material.FindGroup("groupA") != nullptr); @@ -1208,8 +1222,6 @@ namespace UnitTest EXPECT_EQ(material.m_description, "This is a general description about the material"); - EXPECT_EQ(material.m_propertyLayout.m_version, 2); - EXPECT_EQ(material.m_propertyLayout.m_groups.size(), 2); EXPECT_TRUE(material.FindGroup("groupA") != nullptr); EXPECT_TRUE(material.FindGroup("groupB") != nullptr); @@ -1266,7 +1278,6 @@ namespace UnitTest { "description": "", "propertyLayout": { - "version": 2, "groups": [ { "name": "general", @@ -1305,4 +1316,191 @@ namespace UnitTest CheckPropertyValue>(materialTypeAsset, Name{ "general.absolute" }, m_testImageAsset2); CheckPropertyValue>(materialTypeAsset, Name{ "general.relative" }, m_testImageAsset2); } + + + TEST_F(MaterialTypeSourceDataTests, FindPropertyUsingOldName) + { + const AZStd::string inputJson = R"( + { + "version": 10, + "versionUpdates": [ + { + "toVersion": 2, + "actions": [ + { "op": "rename", "from": "general.fooA", "to": "general.fooB" } + ] + }, + { + "toVersion": 4, + "actions": [ + { "op": "rename", "from": "general.barA", "to": "general.barB" } + ] + }, + { + "toVersion": 6, + "actions": [ + { "op": "rename", "from": "general.fooB", "to": "general.fooC" }, + { "op": "rename", "from": "general.barB", "to": "general.barC" } + ] + }, + { + "toVersion": 7, + "actions": [ + { "op": "rename", "from": "general.bazA", "to": "otherGroup.bazB" } + ] + } + ], + "propertyLayout": { + "properties": { + "general": [ + { + "name": "fooC", + "type": "Bool" + }, + { + "name": "barC", + "type": "Float" + } + ], + "otherGroup": [ + { + "name": "dontMindMe", + "type": "Bool" + }, + { + "name": "bazB", + "type": "Float" + } + ] + } + } + } + )"; + + MaterialTypeSourceData materialType; + JsonTestResult loadResult = LoadTestDataFromJson(materialType, inputJson); + + EXPECT_EQ(materialType.m_version, 10); + + // First find the properties using their correct current names + const MaterialTypeSourceData::PropertyDefinition* foo = materialType.FindProperty("general", "fooC"); + const MaterialTypeSourceData::PropertyDefinition* bar = materialType.FindProperty("general", "barC"); + const MaterialTypeSourceData::PropertyDefinition* baz = materialType.FindProperty("otherGroup", "bazB"); + + EXPECT_TRUE(foo); + EXPECT_TRUE(bar); + EXPECT_TRUE(baz); + EXPECT_EQ(foo->m_name, "fooC"); + EXPECT_EQ(bar->m_name, "barC"); + EXPECT_EQ(baz->m_name, "bazB"); + + // Now try doing the property lookup using old versions of the name and make sure the same property can be found + + EXPECT_EQ(foo, materialType.FindProperty("general", "fooA")); + EXPECT_EQ(foo, materialType.FindProperty("general", "fooB")); + EXPECT_EQ(bar, materialType.FindProperty("general", "barA")); + EXPECT_EQ(bar, materialType.FindProperty("general", "barB")); + EXPECT_EQ(baz, materialType.FindProperty("general", "bazA")); + + EXPECT_EQ(nullptr, materialType.FindProperty("general", "fooX")); + EXPECT_EQ(nullptr, materialType.FindProperty("general", "barX")); + EXPECT_EQ(nullptr, materialType.FindProperty("general", "bazX")); + EXPECT_EQ(nullptr, materialType.FindProperty("general", "bazB")); + EXPECT_EQ(nullptr, materialType.FindProperty("otherGroup", "bazA")); + } + + TEST_F(MaterialTypeSourceDataTests, FindPropertyUsingOldName_Error_UnsupportedVersionUpdate) + { + const AZStd::string inputJson = R"( + { + "version": 10, + "versionUpdates": [ + { + "toVersion": 2, + "actions": [ + { "op": "notRename", "from": "general.fooA", "to": "general.fooB" } + ] + } + ], + "propertyLayout": { + "properties": { + "general": [ + { + "name": "fooB", + "type": "Bool" + } + ] + } + } + } + )"; + + MaterialTypeSourceData materialType; + JsonTestResult loadResult = LoadTestDataFromJson(materialType, inputJson); + + ErrorMessageFinder errorMessageFinder; + errorMessageFinder.AddExpectedErrorMessage("Unsupported material version update operation 'notRename'"); + + + const MaterialTypeSourceData::PropertyDefinition* foo = materialType.FindProperty("general", "fooA"); + + EXPECT_EQ(nullptr, foo); + + errorMessageFinder.CheckExpectedErrorsFound(); + } + + TEST_F(MaterialTypeSourceDataTests, CreateMaterialTypeAsset_Error_UnsupportedVersionUpdate) + { + MaterialTypeSourceData sourceData; + + MaterialTypeSourceData::PropertyDefinition propertySource; + propertySource.m_name = "a"; + propertySource.m_dataType = MaterialPropertyDataType::Int; + propertySource.m_value = 0; + sourceData.m_propertyLayout.m_properties["general"].push_back(propertySource); + + sourceData.m_version = 2; + + MaterialTypeSourceData::VersionUpdateDefinition versionUpdate; + versionUpdate.m_toVersion = 2; + MaterialTypeSourceData::VersionUpdatesRenameOperationDefinition updateAction; + updateAction.m_operation = "operationNotKnown"; + versionUpdate.m_actions.push_back(updateAction); + sourceData.m_versionUpdates.push_back(versionUpdate); + + ErrorMessageFinder errorMessageFinder; + errorMessageFinder.AddExpectedErrorMessage("Unsupported material version update operation 'operationNotKnown'"); + errorMessageFinder.AddIgnoredErrorMessage("Failed to build MaterialTypeAsset", true); + + auto materialTypeOutcome = sourceData.CreateMaterialTypeAsset(Uuid::CreateRandom()); + EXPECT_FALSE(materialTypeOutcome.IsSuccess()); + + errorMessageFinder.CheckExpectedErrorsFound(); + } + + TEST_F(MaterialTypeSourceDataTests, CreateMaterialTypeAsset_Error_VersionInWrongLocation) + { + // The version field used to be under the propertyLayout section, but it has been moved up to the top level. + // If any users have their own custom .materialtype with an older format that has the version in the wrong place + // then we will report an error with instructions to move it to the correct location. + + ErrorMessageFinder errorMessageFinder; + errorMessageFinder.AddExpectedErrorMessage("The field '/propertyLayout/version' is deprecated and moved to '/version'. Please edit this material type source file and move the '\"version\": 4' setting up one level"); + + const AZStd::string inputJson = R"( + { + "propertyLayout": { + "version": 4 + } + } + )"; + + MaterialTypeSourceData materialType; + JsonTestResult loadResult = LoadTestDataFromJson(materialType, inputJson); + + auto materialTypeOutcome = materialType.CreateMaterialTypeAsset(Uuid::CreateRandom()); + EXPECT_FALSE(materialTypeOutcome.IsSuccess()); + + errorMessageFinder.CheckExpectedErrorsFound(); + } } diff --git a/Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake b/Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake index 49c7231fed..4f0e432511 100644 --- a/Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake +++ b/Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake @@ -61,6 +61,7 @@ set(FILES Include/Atom/RPI.Reflect/Material/MaterialTypeAssetCreator.h Include/Atom/RPI.Reflect/Material/ShaderCollection.h Include/Atom/RPI.Reflect/Material/MaterialFunctor.h + Include/Atom/RPI.Reflect/Material/MaterialVersionUpdate.h Include/Atom/RPI.Reflect/Pass/ComputePassData.h Include/Atom/RPI.Reflect/Pass/CopyPassData.h Include/Atom/RPI.Reflect/Pass/DownsampleMipChainPassData.h @@ -141,6 +142,7 @@ set(FILES Source/RPI.Reflect/Material/MaterialTypeAssetCreator.cpp Source/RPI.Reflect/Material/ShaderCollection.cpp Source/RPI.Reflect/Material/MaterialFunctor.cpp + Source/RPI.Reflect/Material/MaterialVersionUpdate.cpp Source/RPI.Reflect/Pass/PassAsset.cpp Source/RPI.Reflect/Pass/PassAttachmentReflect.cpp Source/RPI.Reflect/Pass/PassRequest.cpp diff --git a/Gems/Atom/TestData/TestData/Materials/Types/AutoBrick.materialtype b/Gems/Atom/TestData/TestData/Materials/Types/AutoBrick.materialtype index 00f11663f7..cf0bffa058 100644 --- a/Gems/Atom/TestData/TestData/Materials/Types/AutoBrick.materialtype +++ b/Gems/Atom/TestData/TestData/Materials/Types/AutoBrick.materialtype @@ -1,7 +1,7 @@ { "description": "This is an example of a custom material type using Atom's PBR shading model: procedurally generated brick or tile.", + "version": 3, "propertyLayout": { - "version": 3, "groups": [ { "name": "shape", diff --git a/Gems/Atom/TestData/TestData/Materials/Types/MinimalPBR.materialtype b/Gems/Atom/TestData/TestData/Materials/Types/MinimalPBR.materialtype index 81ebd63c28..5d99737576 100644 --- a/Gems/Atom/TestData/TestData/Materials/Types/MinimalPBR.materialtype +++ b/Gems/Atom/TestData/TestData/Materials/Types/MinimalPBR.materialtype @@ -1,7 +1,7 @@ { "description": "Base Material with properties used to define Standard PBR, a metallic-roughness Physically-Based Rendering (PBR) material shading model.", + "version": 3, "propertyLayout": { - "version": 3, "groups": [ { "name": "settings", diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/CMakeLists.txt b/Gems/Atom/Tools/AtomToolsFramework/Code/CMakeLists.txt index ea5b65be7c..e64c9b80e7 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/CMakeLists.txt +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/CMakeLists.txt @@ -10,6 +10,8 @@ if(NOT PAL_TRAIT_BUILD_HOST_TOOLS) return() endif() +ly_get_list_relative_pal_filename(pal_source_dir ${CMAKE_CURRENT_LIST_DIR}/Source/Platform/${PAL_PLATFORM_NAME}) + ly_add_target( NAME AtomToolsFramework.Static STATIC NAMESPACE Gem @@ -18,9 +20,11 @@ ly_add_target( AUTORCC FILES_CMAKE atomtoolsframework_files.cmake + ${pal_source_dir}/platform_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake INCLUDE_DIRECTORIES PRIVATE Source + ${pal_source_dir} PUBLIC Include BUILD_DEPENDENCIES diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/RenderViewportWidget.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/RenderViewportWidget.h index 268285adc0..8233658ded 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/RenderViewportWidget.h +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/RenderViewportWidget.h @@ -102,6 +102,8 @@ namespace AtomToolsFramework void BeginCursorCapture() override; void EndCursorCapture() override; bool IsMouseOver() const override; + void SetOverrideCursor(AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride) override; + void ClearOverrideCursor() override; // AzFramework::WindowRequestBus::Handler overrides ... void SetWindowTitle(const AZStd::string& title) override; diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp index 3fae2ee7a3..e8ec77e7bd 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp @@ -36,6 +36,8 @@ #include #include +#include "AtomToolsFramework_Traits_Platform.h" + AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT #include #include @@ -216,7 +218,11 @@ namespace AtomToolsFramework AtomToolsMainWindowNotificationBus::Handler::BusDisconnect(); AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::StartDisconnectingAssetProcessor); +#if AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY + ::_exit(0); +#else Base::Destroy(); +#endif } AZStd::vector AtomToolsApplication::GetCriticalAssetFilters() const diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/AtomToolsFramework_Traits_Linux.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/AtomToolsFramework_Traits_Linux.h new file mode 100644 index 0000000000..2f4c787fbe --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/AtomToolsFramework_Traits_Linux.h @@ -0,0 +1,15 @@ +/* + * 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 + +// On some platforms, there is an issue with environment variables that are removed before some objects are deallocated (during the process of +// ComponentApplication::Destroy). Until all of the shutdown issues are solved, the following trait will skip the parent ::Destroy() and exit +// the application as soon as possible if set to true. +// (Tracked by GHI - 4806) +#define AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY true + diff --git a/Gems/AudioEngineWwise/Code/Source/AudioEngineWwiseModule_Stub.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/AtomToolsFramework_Traits_Platform.h similarity index 70% rename from Gems/AudioEngineWwise/Code/Source/AudioEngineWwiseModule_Stub.cpp rename to Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/AtomToolsFramework_Traits_Platform.h index 4344eb6072..8101a49a5a 100644 --- a/Gems/AudioEngineWwise/Code/Source/AudioEngineWwiseModule_Stub.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/AtomToolsFramework_Traits_Platform.h @@ -5,7 +5,6 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT * */ +#pragma once -#include - -AZ_DECLARE_MODULE_CLASS(Gem_AudioEngineWwise, AZ::Module) +#include diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/max_materials.py b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/platform_linux_files.cmake old mode 100755 new mode 100644 similarity index 51% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/max_materials.py rename to Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/platform_linux_files.cmake index a67c39910e..957ef8663e --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/max_materials.py +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/platform_linux_files.cmake @@ -1,3 +1,4 @@ +# # 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. # @@ -5,14 +6,7 @@ # # - -import MaxPlus -import sys - - -def get_material_information(): - for mesh_object in MaxPlus.Core.GetRootNode().Children: - print('Object---> {}'.format(mesh_object)) - - -get_material_information() +set(FILES + AtomToolsFramework_Traits_Platform.h + AtomToolsFramework_Traits_Linux.h +) diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/AtomToolsFramework_Traits_Mac.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/AtomToolsFramework_Traits_Mac.h new file mode 100644 index 0000000000..3ca246c797 --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/AtomToolsFramework_Traits_Mac.h @@ -0,0 +1,15 @@ +/* + * 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 + +// On some platforms, there is an issue with environment variables that are removed before some objects are deallocated (during the process of +// ComponentApplication::Destroy). Until all of the shutdown issues are solved, the following trait will skip the parent ::Destroy() and exit +// the application as soon as possible if set to true. +// (Tracked by GHI - 4806) +#define AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY false + diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/AtomToolsFramework_Traits_Platform.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/AtomToolsFramework_Traits_Platform.h new file mode 100644 index 0000000000..6d8c8d7e32 --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/AtomToolsFramework_Traits_Platform.h @@ -0,0 +1,10 @@ +/* + * 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 diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/platform_mac_files.cmake b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/platform_mac_files.cmake new file mode 100644 index 0000000000..13cc6886ef --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/platform_mac_files.cmake @@ -0,0 +1,12 @@ +# +# 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 + AtomToolsFramework_Traits_Platform.h + AtomToolsFramework_Traits_Mac.h +) diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/AtomToolsFramework_Traits_Platform.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/AtomToolsFramework_Traits_Platform.h new file mode 100644 index 0000000000..ac82be7874 --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/AtomToolsFramework_Traits_Platform.h @@ -0,0 +1,10 @@ +/* + * 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 diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/AtomToolsFramework_Traits_Windows.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/AtomToolsFramework_Traits_Windows.h new file mode 100644 index 0000000000..3ca246c797 --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/AtomToolsFramework_Traits_Windows.h @@ -0,0 +1,15 @@ +/* + * 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 + +// On some platforms, there is an issue with environment variables that are removed before some objects are deallocated (during the process of +// ComponentApplication::Destroy). Until all of the shutdown issues are solved, the following trait will skip the parent ::Destroy() and exit +// the application as soon as possible if set to true. +// (Tracked by GHI - 4806) +#define AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY false + diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/platform_windows_files.cmake b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/platform_windows_files.cmake new file mode 100644 index 0000000000..e2a2113bf9 --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/platform_windows_files.cmake @@ -0,0 +1,12 @@ +# +# 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 + AtomToolsFramework_Traits_Platform.h + AtomToolsFramework_Traits_Windows.h +) diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp index a0d2034082..2b39a87623 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp @@ -61,7 +61,7 @@ namespace AtomToolsFramework AZ::RPI::RenderPipelineDescriptor pipelineDesc; pipelineDesc.m_mainViewTagName = "MainCamera"; pipelineDesc.m_name = pipelineName; - pipelineDesc.m_rootPassTemplate = "MainPipelineRenderToTexture"; + pipelineDesc.m_rootPassTemplate = "ToolsPipelineRenderToTexture"; // We have to set the samples to 4 to match the pipeline passes' setting, otherwise it may lead to device lost issue // [GFX TODO] [ATOM-13551] Default value sand validation required to prevent pipeline crash and device lost diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp index e8975c0b62..9672abfd99 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp @@ -196,12 +196,8 @@ namespace AtomToolsFramework bool RenderViewportWidget::event(QEvent* event) { - // On some types of QEvents, a resize event is needed to make sure that the current viewport window - // needs to be updated based on a potential new surface dimensions. switch (event->type()) { - case QEvent::ScreenChangeInternal: - case QEvent::UpdateLater: case QEvent::Resize: SendWindowResizeEvent(); break; @@ -375,6 +371,16 @@ namespace AtomToolsFramework m_inputChannelMapper->SetCursorCaptureEnabled(false); } + void RenderViewportWidget::SetOverrideCursor(AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride) + { + m_inputChannelMapper->SetOverrideCursor(cursorStyleOverride); + } + + void RenderViewportWidget::ClearOverrideCursor() + { + m_inputChannelMapper->ClearOverrideCursor(); + } + void RenderViewportWidget::SetWindowTitle(const AZStd::string& title) { setWindowTitle(QString::fromUtf8(title.c_str())); diff --git a/Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.cpp b/Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.cpp index 17e292ac16..98af749261 100644 --- a/Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.cpp +++ b/Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.cpp @@ -230,9 +230,11 @@ namespace MaterialEditor // create source data from properties MaterialSourceData sourceData; - sourceData.m_propertyLayoutVersion = m_materialTypeSourceData.m_propertyLayout.m_version; sourceData.m_materialType = m_materialSourceData.m_materialType; sourceData.m_parentMaterial = m_materialSourceData.m_parentMaterial; + + AZ_Assert(m_materialAsset && m_materialAsset->GetMaterialTypeAsset(), "When IsOpen() is true, these assets should not be null."); + sourceData.m_materialTypeVersion = m_materialAsset->GetMaterialTypeAsset()->GetVersion(); // Force save data to store forward slashes AzFramework::StringFunc::Replace(sourceData.m_materialType, "\\", "/"); @@ -302,9 +304,11 @@ namespace MaterialEditor // create source data from properties MaterialSourceData sourceData; - sourceData.m_propertyLayoutVersion = m_materialTypeSourceData.m_propertyLayout.m_version; sourceData.m_materialType = m_materialSourceData.m_materialType; sourceData.m_parentMaterial = m_materialSourceData.m_parentMaterial; + + AZ_Assert(m_materialAsset && m_materialAsset->GetMaterialTypeAsset(), "When IsOpen() is true, these assets should not be null."); + sourceData.m_materialTypeVersion = m_materialAsset->GetMaterialTypeAsset()->GetVersion(); // Force save data to store forward slashes AzFramework::StringFunc::Replace(sourceData.m_materialType, "\\", "/"); @@ -373,8 +377,10 @@ namespace MaterialEditor // create source data from properties MaterialSourceData sourceData; - sourceData.m_propertyLayoutVersion = m_materialTypeSourceData.m_propertyLayout.m_version; sourceData.m_materialType = m_materialSourceData.m_materialType; + + AZ_Assert(m_materialAsset && m_materialAsset->GetMaterialTypeAsset(), "When IsOpen() is true, these assets should not be null."); + sourceData.m_materialTypeVersion = m_materialAsset->GetMaterialTypeAsset()->GetVersion(); // Only assign a parent path if the source was a .material if (AzFramework::StringFunc::Path::IsExtension(m_relativePath.c_str(), MaterialSourceData::Extension)) @@ -679,6 +685,12 @@ namespace MaterialEditor return false; } m_materialTypeSourceData = materialTypeOutcome.GetValue(); + + if (MaterialSourceData::ApplyVersionUpdatesResult::Failed == m_materialSourceData.ApplyVersionUpdates(m_absolutePath)) + { + AZ_Error("MaterialDocument", false, "Material source data could not be auto updated to the latest version of the material type: '%s'.", m_materialSourceData.m_materialType.c_str()); + return false; + } } else if (AzFramework::StringFunc::Path::IsExtension(m_absolutePath.c_str(), MaterialTypeSourceData::Extension)) { diff --git a/Gems/AtomLyIntegration/AtomBridge/Assets/Shaders/LyShineUI.shader b/Gems/AtomLyIntegration/AtomBridge/Assets/Shaders/LyShineUI.shader index 9fd3e76813..14088e8c1a 100644 --- a/Gems/AtomLyIntegration/AtomBridge/Assets/Shaders/LyShineUI.shader +++ b/Gems/AtomLyIntegration/AtomBridge/Assets/Shaders/LyShineUI.shader @@ -1,5 +1,5 @@ { - "Source" : "LyShineUI", + "Source" : "LyShineUI.azsl", "DepthStencilState" : { "Depth" : { diff --git a/Gems/AtomLyIntegration/AtomBridge/Assets/Shaders/SimpleTextured.shader b/Gems/AtomLyIntegration/AtomBridge/Assets/Shaders/SimpleTextured.shader index 0f4090c08e..0932ea00b7 100644 --- a/Gems/AtomLyIntegration/AtomBridge/Assets/Shaders/SimpleTextured.shader +++ b/Gems/AtomLyIntegration/AtomBridge/Assets/Shaders/SimpleTextured.shader @@ -1,5 +1,5 @@ { - "Source" : "SimpleTextured", + "Source" : "SimpleTextured.azsl", "DepthStencilState" : { "Depth" : { diff --git a/Gems/AtomLyIntegration/AtomViewportDisplayIcons/Assets/Shaders/TexturedIcon.shader b/Gems/AtomLyIntegration/AtomViewportDisplayIcons/Assets/Shaders/TexturedIcon.shader index 601a2664b5..3793d9c8ed 100644 --- a/Gems/AtomLyIntegration/AtomViewportDisplayIcons/Assets/Shaders/TexturedIcon.shader +++ b/Gems/AtomLyIntegration/AtomViewportDisplayIcons/Assets/Shaders/TexturedIcon.shader @@ -1,5 +1,5 @@ { - "Source" : "TexturedIcon", + "Source" : "TexturedIcon.azsl", "DepthStencilState" : { "Depth" : { diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h index a8088c63ac..3cafc183a5 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h @@ -168,6 +168,22 @@ namespace AZ //! Sets whether the directional shadowmap should use receiver plane bias. //! @param enable flag specifying whether to enable the receiver plane bias feature virtual void SetShadowReceiverPlaneBiasEnabled(bool enable) = 0; + + //! Shadow bias reduces acne by applying a small amount of offset along shadow-space z. + //! @return Returns the amount of bias to apply. + virtual float GetShadowBias() const = 0; + + //! Shadow bias reduces acne by applying a small amount of offset along shadow-space z. + //! @param Sets the amount of bias to apply. + virtual void SetShadowBias(float bias) = 0; + + //! Reduces acne by biasing the shadowmap lookup along the geometric normal. + //! @return Returns the amount of bias to apply. + virtual float GetNormalShadowBias() const = 0; + + //! Reduces acne by biasing the shadowmap lookup along the geometric normal. + //! @param normalShadowBias Sets the amount of normal shadow bias to apply. + virtual void SetNormalShadowBias(float normalShadowBias) = 0; }; using DirectionalLightRequestBus = EBus; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h index a58acc0114..92d5cc9ac0 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h @@ -101,6 +101,9 @@ namespace AZ //! Method of shadow's filtering. ShadowFilterMethod m_shadowFilterMethod = ShadowFilterMethod::None; + // Reduces acne by biasing the shadowmap lookup along the geometric normal. + float m_normalShadowBias = 0.0f; + //! Sample Count for filtering (from 4 to 64) //! It is used only when the pixel is predicted as on the boundary. uint16_t m_filteringSampleCount = 32; @@ -109,6 +112,9 @@ namespace AZ //! This uses partial derivatives to reduce shadow acne when using large pcf kernels. bool m_receiverPlaneBiasEnabled = true; + //! Reduces shadow acne by applying a small amount of offset along shadow-space z. + float m_shadowBias = 0.0f; + bool IsSplitManual() const; bool IsSplitAutomatic() const; bool IsCascadeCorrectionDisabled() const; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp index 9d384c2e24..78a9cc21d1 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp @@ -38,7 +38,9 @@ namespace AZ ->Field("IsDebugColoringEnabled", &DirectionalLightComponentConfig::m_isDebugColoringEnabled) ->Field("ShadowFilterMethod", &DirectionalLightComponentConfig::m_shadowFilterMethod) ->Field("PcfFilteringSampleCount", &DirectionalLightComponentConfig::m_filteringSampleCount) - ->Field("ShadowReceiverPlaneBiasEnabled", &DirectionalLightComponentConfig::m_receiverPlaneBiasEnabled); + ->Field("ShadowReceiverPlaneBiasEnabled", &DirectionalLightComponentConfig::m_receiverPlaneBiasEnabled) + ->Field("Shadow Bias", &DirectionalLightComponentConfig::m_shadowBias) + ->Field("Normal Shadow Bias", &DirectionalLightComponentConfig::m_normalShadowBias); } } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp index 78558cfc85..e36868c4bb 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp @@ -84,6 +84,10 @@ namespace AZ ->Event("SetFilteringSampleCount", &DirectionalLightRequestBus::Events::SetFilteringSampleCount) ->Event("GetShadowReceiverPlaneBiasEnabled", &DirectionalLightRequestBus::Events::GetShadowReceiverPlaneBiasEnabled) ->Event("SetShadowReceiverPlaneBiasEnabled", &DirectionalLightRequestBus::Events::SetShadowReceiverPlaneBiasEnabled) + ->Event("GetShadowBias", &DirectionalLightRequestBus::Events::GetShadowBias) + ->Event("SetShadowBias", &DirectionalLightRequestBus::Events::SetShadowBias) + ->Event("GetNormalShadowBias", &DirectionalLightRequestBus::Events::GetNormalShadowBias) + ->Event("SetNormalShadowBias", &DirectionalLightRequestBus::Events::SetNormalShadowBias) ->VirtualProperty("Color", "GetColor", "SetColor") ->VirtualProperty("Intensity", "GetIntensity", "SetIntensity") ->VirtualProperty("AngularDiameter", "GetAngularDiameter", "SetAngularDiameter") @@ -98,7 +102,9 @@ namespace AZ ->VirtualProperty("DebugColoringEnabled", "GetDebugColoringEnabled", "SetDebugColoringEnabled") ->VirtualProperty("ShadowFilterMethod", "GetShadowFilterMethod", "SetShadowFilterMethod") ->VirtualProperty("FilteringSampleCount", "GetFilteringSampleCount", "SetFilteringSampleCount") - ->VirtualProperty("ShadowReceiverPlaneBiasEnabled", "GetShadowReceiverPlaneBiasEnabled", "SetShadowReceiverPlaneBiasEnabled"); + ->VirtualProperty("ShadowReceiverPlaneBiasEnabled", "GetShadowReceiverPlaneBiasEnabled", "SetShadowReceiverPlaneBiasEnabled") + ->VirtualProperty("ShadowBias", "GetShadowBias", "SetShadowBias") + ->VirtualProperty("NormalShadowBias", "GetNormalShadowBias", "SetNormalShadowBias"); ; } } @@ -406,6 +412,34 @@ namespace AZ return aznumeric_cast(m_configuration.m_filteringSampleCount); } + void DirectionalLightComponentController::SetShadowBias(float bias) + { + m_configuration.m_shadowBias = bias; + if (m_featureProcessor) + { + m_featureProcessor->SetShadowBias(m_lightHandle, bias); + } + } + + float DirectionalLightComponentController::GetShadowBias() const + { + return m_configuration.m_shadowBias; + } + + void DirectionalLightComponentController::SetNormalShadowBias(float bias) + { + m_configuration.m_normalShadowBias = bias; + if (m_featureProcessor) + { + m_featureProcessor->SetNormalShadowBias(m_lightHandle, bias); + } + } + + float DirectionalLightComponentController::GetNormalShadowBias() const + { + return m_configuration.m_normalShadowBias; + } + void DirectionalLightComponentController::SetFilteringSampleCount(uint32_t count) { const uint16_t count16 = GetMin(Shadow::MaxPcfSamplingCount, aznumeric_cast(count)); @@ -499,6 +533,8 @@ namespace AZ SetViewFrustumCorrectionEnabled(m_configuration.m_isCascadeCorrectionEnabled); SetDebugColoringEnabled(m_configuration.m_isDebugColoringEnabled); SetShadowFilterMethod(m_configuration.m_shadowFilterMethod); + SetShadowBias(m_configuration.m_shadowBias); + SetNormalShadowBias(m_configuration.m_normalShadowBias); SetFilteringSampleCount(m_configuration.m_filteringSampleCount); SetShadowReceiverPlaneBiasEnabled(m_configuration.m_receiverPlaneBiasEnabled); diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h index 933f2705e7..9a6edda666 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h @@ -80,6 +80,10 @@ namespace AZ void SetFilteringSampleCount(uint32_t count) override; bool GetShadowReceiverPlaneBiasEnabled() const override; void SetShadowReceiverPlaneBiasEnabled(bool enable) override; + float GetShadowBias() const override; + void SetShadowBias(float bias) override; + float GetNormalShadowBias() const override; + void SetNormalShadowBias(float bias) override; private: friend class EditorDirectionalLightComponent; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorAreaLightComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorAreaLightComponent.cpp index 1e5b2580f7..db46434d9a 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorAreaLightComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorAreaLightComponent.cpp @@ -136,7 +136,7 @@ namespace AZ ->Attribute(Edit::Attributes::Min, 0.0f) ->Attribute(Edit::Attributes::Max, 100.0f) ->Attribute(Edit::Attributes::SoftMin, 0.0f) - ->Attribute(Edit::Attributes::SoftMax, 1.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) ->Attribute(Edit::Attributes::Visibility, &AreaLightComponentConfig::SupportsShadows) ->Attribute(Edit::Attributes::ReadOnly, &AreaLightComponentConfig::ShadowsDisabled) diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp index 69ba295e9b..545064b86f 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp @@ -133,8 +133,8 @@ namespace AZ ->EnumAttribute(ShadowFilterMethod::Esm, "ESM") ->EnumAttribute(ShadowFilterMethod::EsmPcf, "ESM+PCF") ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) - ->DataElement(Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_filteringSampleCount, "Filtering sample count", - "This is used only when the pixel is predicted as on the boundary. " + ->DataElement(Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_filteringSampleCount, "Filtering sample count\n", + "This is used only when the pixel is predicted to be on the boundary.\n" "Specific to PCF and ESM+PCF.") ->Attribute(Edit::Attributes::Min, 4) ->Attribute(Edit::Attributes::Max, 64) @@ -142,10 +142,26 @@ namespace AZ ->Attribute(Edit::Attributes::ReadOnly, &DirectionalLightComponentConfig::IsShadowPcfDisabled) ->DataElement( Edit::UIHandlers::CheckBox, &DirectionalLightComponentConfig::m_receiverPlaneBiasEnabled, - "Shadow Receiver Plane Bias Enable", + "Shadow Receiver Plane Bias Enable\n", "This reduces shadow acne when using large pcf kernels.") ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) - ->Attribute(Edit::Attributes::ReadOnly, &DirectionalLightComponentConfig::IsShadowPcfDisabled); + ->Attribute(Edit::Attributes::ReadOnly, &DirectionalLightComponentConfig::IsShadowPcfDisabled) + ->DataElement( + Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_shadowBias, + "Shadow Bias\n", + "Reduces acne by applying a fixed bias along z in shadow-space.\n" + "If this is 0, no biasing is applied.") + ->Attribute(Edit::Attributes::Min, 0.f) + ->Attribute(Edit::Attributes::Max, 0.2) + ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) + ->DataElement( + Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_normalShadowBias, "Normal Shadow Bias\n", + "Reduces acne by biasing the shadowmap lookup along the geometric normal.\n" + "If this is 0, no biasing is applied.") + ->Attribute(Edit::Attributes::Min, 0.f) + ->Attribute(Edit::Attributes::Max, 10.0f) + ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) + ; } } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentUtil.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentUtil.cpp index 070c42fd7c..d15db886d6 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentUtil.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentUtil.cpp @@ -99,7 +99,6 @@ namespace AZ { // Construct the material source data object that will be exported AZ::RPI::MaterialSourceData exportData; - exportData.m_propertyLayoutVersion = editData.m_materialTypeSourceData.m_propertyLayout.m_version; // Converting absolute material paths to relative paths bool result = false; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp index 746b1ff7e5..e53010d747 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp @@ -194,6 +194,8 @@ namespace AZ AZ::RPI::AssetUtils::GetAssetIdForProductPath(DefaultLightingPresetPath), propertyOverrides), [entityId, materialAssignmentId]() { + AZ_UNUSED(entityId); + AZ_UNUSED(materialAssignmentId); AZ_Warning( "EditorMaterialSystemComponent", false, "RenderMaterialPreview capture failed for entity %s slot %s.", entityId.ToString().c_str(), materialAssignmentId.ToString().c_str()); diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp index 2d8f83a32a..3cc9535d7a 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp @@ -100,9 +100,13 @@ namespace AZ ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_whiteBalanceKelvin, "Temperature", "Temperature in Kelvin") ->Attribute(Edit::Attributes::Min, 1000.0f) ->Attribute(Edit::Attributes::Max, 40000.0f) + ->Attribute(AZ::Edit::Attributes::SliderCurveMidpoint, 0.165f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_whiteBalanceTint, "Tint", "Tint Value") ->Attribute(Edit::Attributes::Min, -100.0f) ->Attribute(Edit::Attributes::Max, 100.0f) + ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_whiteBalanceLuminancePreservation, "Luminance Preservation", "Modulate the preservation of luminance") + ->Attribute(Edit::Attributes::Min, 0.0f) + ->Attribute(Edit::Attributes::Max, 1.0f) ->ClassElement(AZ::Edit::ClassElements::Group, "Split Toning") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) @@ -131,16 +135,20 @@ namespace AZ ->Attribute(Edit::Attributes::Max, 1.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhShadowsStart, "Shadows Start", "SMH Shadows Start Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhShadowsEnd, "Shadows End", "SMH Shadows End Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhHighlightsStart, "Highlights Start", "SMH Highlights Start Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhHighlightsEnd, "Highlights End", "SMH Highlights End Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhShadowsColor, "Shadows Color", "SMH Shadows Color") ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhMidtonesColor, "Midtones Color", "SMH Midtones Color") ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhHighlightsColor, "Highlights Color", "SMH Highlights Color") diff --git a/Gems/AtomLyIntegration/ImguiAtom/Assets/Shaders/ImGuiAtom/ImGuiAtom.shader b/Gems/AtomLyIntegration/ImguiAtom/Assets/Shaders/ImGuiAtom/ImGuiAtom.shader index db96bfecb7..2f2a81ea3d 100644 --- a/Gems/AtomLyIntegration/ImguiAtom/Assets/Shaders/ImGuiAtom/ImGuiAtom.shader +++ b/Gems/AtomLyIntegration/ImguiAtom/Assets/Shaders/ImGuiAtom/ImGuiAtom.shader @@ -1,6 +1,6 @@ { - "Source" : "ImGuiAtom", + "Source" : "ImGuiAtom.azsl", "RasterState" : { "CullMode" : "None" }, diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.env b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.env.example similarity index 63% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.env rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.env.example index 080b4e92f7..29c1739992 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.env +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.env.example @@ -4,18 +4,18 @@ # SPDX-License-Identifier: Apache-2.0 OR MIT # # -# -- This line is 75 characters ------------------------------------------- +# ------------------------------------------------------------------------- # Sets up the project environment for python scripting using the export DYNACONF_COMPANY=Amazon -# if a lumberyard project isn't set use this gem -export DYNACONF_LY_PROJECT=DccScriptingInterface -export DYNACONF_LY_PROJECT_PATH=`pwd` -export DYNACONF_LY_DEV=${LY_PROJECT_PATH}\..\..\..\.. +# if a O3DE project isn't set use this gem +export DYNACONF_O3DE_PROJECT=DccScriptingInterface +export DYNACONF_O3DE_PROJECT_PATH=`pwd` +export DYNACONF_O3DE_DEV=${O3DE_PROJECT_PATH}\..\..\..\.. # LY build folder -export DYNACONF_LY_BUILD_PATH=${LY_DEV}\build -export DYNACONF_LY_BIN_PATH=${LY_BUILD_PATH}\bin\profile +export DYNACONF_O3DE_BUILD_PATH=${O3DE_DEV}\build +export DYNACONF_O3DE_BIN_PATH=${O3DE_BUILD_PATH}\bin\profile # default IDE and debug settings #export DYNACONF_DCCSI_GDEBUG=false @@ -24,9 +24,9 @@ export DYNACONF_DCCSI_GDEBUGGER=WING export DYNACONF_DCCSI_LOGLEVEL=20 # defaults for DccScriptingInterface (DCCsi) -export DYNACONF_DCCSIG_PATH=${LY_DEV}\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface +export DYNACONF_DCCSIG_PATH=${O3DE_DEV}\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface -# set up default python interpreter (Lumberyard) +# set up default python interpreter (O3DE) # we may want to entirely remove these and rely on config.py to dynamically set up # however VScode can be configured with a .env so might be valueable to keep export DYNACONF_DCCSI_PY_VERSION_MAJOR=3 @@ -40,13 +40,13 @@ export DYNACONF_DCCSI_PYTHON_PATH=${DCCSIG_PATH}\3rdParty\Python export DYNACONF_DCCSI_PYTHON_LIB_PATH=${DCCSI_PYTHON_PATH}\Lib\${DCCSI_PY_VERSION_MAJOR}.x\${DCCSI_PY_VERSION_MAJOR}.${DCCSI_PY_VERSION_MINOR}.x\site-packages # TO DO: figure out how to best deal with OS folder (i.e. 'windows') -export DYNACONF_DCCSI_PYTHON_INSTALL=${LY_DEV}\python -export DYNACONF_DDCCSI_PY_BASE=${DCCSI_PYTHON_INSTALL}\python.cmd +export DYNACONF_O3DE_PYTHON_INSTALL=${O3DE_DEV}\python +export DYNACONF_DCCSI_PY_BASE=${O3DE_PYTHON_INSTALL}\python.cmd # set up Qt / PySide2 # TO DO: These should NOT be set in the global env as they will cause conflicts # with other Qt apps (like DCC tools), only set in local.env, or modify config.py # for utils/tools/apps that need them ( see config.init_ly_pyside() ) -#export DYNACONF_QTFORPYTHON_PATH=${LY_DEV}\Gems\QtForPython\3rdParty\pyside2\windows\release -#export DYNACONF_QT_PLUGIN_PATH=${LY_BUILD_PATH}\bin\profile\EditorPlugins -#export DYNACONF_QT_QPA_PLATFORM_PLUGIN_PATH=${LY_BUILD_PATH}\bin\profile\EditorPlugins\platforms +#export DYNACONF_QTFORPYTHON_PATH=${O3DE_DEV}\Gems\QtForPython\3rdParty\pyside2\windows\release +#export DYNACONF_QT_PLUGIN_PATH=${O3DE_BUILD_PATH}\bin\profile\EditorPlugins +#export DYNACONF_QT_QPA_PLATFORM_PLUGIN_PATH=${O3DE_BUILD_PATH}\bin\profile\EditorPlugins\platforms diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.gitignore b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.gitignore index 9ad4717873..086d189eba 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.gitignore +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.gitignore @@ -8,4 +8,6 @@ workspace.xml # Ignore dynaconf secret files .secrets.* settings.local.json -azpy/_sample_package_/* \ No newline at end of file +azpy/_sample_package_/* +.env +settings_export.json.tmp \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.p4ignore b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.p4ignore index dedde06bc4..2ff9b6b614 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.p4ignore +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.p4ignore @@ -19,3 +19,4 @@ __WIP__/* !.gitignore .secrets.* settings.local.json +.env \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/3rdParty/Python/README.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/3rdParty/Python/README.txt new file mode 100644 index 0000000000..7f5a72fb1c --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/3rdParty/Python/README.txt @@ -0,0 +1,36 @@ +DccScriptingInterface (DCCsi) + +This location can be extended with additional 3rdParty Python Utils, Tools, Packages, etc. + +These are not installed or distributed with O3DE + +However, there is some stubbed scaffolding in place. + +This is a bootstrapped sandbox for installing python libs (version bootstrapped procedurally): +C:\Depot\o3de-engine\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\3rdParty\Python\Lib\2.x\2.7.x\site-packages +C:\Depot\o3de-engine\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\3rdParty\Python\Lib\3.x\3.7.x\site-packages + +Any libs installed to this location will be accessible (use at your own risk) + +For instance, if you want to add py2.7 compatible libs, for apps like Maya2020: +"C:\Depot\o3de-engine\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\Tools\DCC\Maya\readme.txt" + +These pyside2-tools can be useful, and they are not pip installed, nor distributed. +C:\Depot\o3de-engine\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\3rdParty\Python\pyside2-tools + +pyside2-tools instructions: + +1. clone the repo in this location: C:\Depot\o3de-engine\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\3rdParty\Python + >git clone https://github.com/pyside/pyside2-tools + +2. to use as a python package ... + find this and copy: + "< local DCCsi >\3rdParty\Python\pyside2-tools\pyside2uic\__init__.py.in" + + and rename to this: + "< local DCCsi >\3rdParty\Python\pyside2-tools\pyside2uic\__init__.py" + +3. add to PYTHONPATH: < local DCCsi >\3rdParty\Python + in .py something like: site.addsitedir(DCCSI_PYSIDE2_TOOLS) + +See: "< local DCCsi >\config.py" \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Editor/Scripts/bootstrap.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Editor/Scripts/bootstrap.py index d417c572e9..406e2b04ff 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Editor/Scripts/bootstrap.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Editor/Scripts/bootstrap.py @@ -7,9 +7,9 @@ # SPDX-License-Identifier: Apache-2.0 OR MIT # # -# -- This line is 75 characters ------------------------------------------- +# ------------------------------------------------------------------------- """This module is for use in boostrapping the DccScriptingInterface Gem -with Lumberyard. Note: this boostrap is only designed fo be py3 compatible. +with O3DE. Note: this boostrap is only designed fo be py3 compatible. If you need DCCsi access in py27 (Autodesk Maya for instance) you may need to implement your own boostrapper module. Currently this is boostrapped from add_dccsi.py, as a temporty measure related to this Jira: @@ -24,40 +24,64 @@ import logging as _logging # ------------------------------------------------------------------------- +# ------------------------------------------------------------------------- +_O3DE_RUNNING=None +try: + import azlmbr + _O3DE_RUNNING=True +except: + _O3DE_RUNNING=False +# ------------------------------------------------------------------------- + + # ------------------------------------------------------------------------- # we don't use dynaconf setting here as we might not yet have access # to that site-dir. -_MODULE = 'DCCsi.bootstrap' +_MODULENAME = __name__ +if _MODULENAME is '__main__': + _MODULENAME = 'O3DE.DCCsi.bootstrap' + +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_MODULENAME) # we need to set up basic access to the DCCsi _MODULE_PATH = os.path.realpath(__file__) # To Do: what if frozen? -_DCCSIG_PATH = os.path.normpath(os.path.join(_MODULE_PATH, '../../..')) -_DCCSIG_PATH = os.getenv('DCCSIG_PATH', _DCCSIG_PATH) -site.addsitedir(_DCCSIG_PATH) - -# we can get basic access to the DCCsi.azpy api now -import azpy - -# early attach WingIDE debugger (can refactor to include other IDEs later) -while 0: # flag on to attemp to connect wingIDE debugger - from azpy.env_bool import env_bool - if not env_bool('DCCSI_DEBUGGER_ATTACHED', False): - # if not already attached lets do it here - from azpy.test.entry_test import connect_wing - foo = connect_wing() +_DCCSI_PATH = os.path.normpath(os.path.join(_MODULE_PATH, '../../..')) +_DCCSI_PATH = os.getenv('DCCSI_PATH', _DCCSI_PATH) +site.addsitedir(_DCCSI_PATH) + +# now we have azpy api access +from azpy.env_bool import env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG +from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import ENVAR_DCCSI_LOGLEVEL +from azpy.constants import FRMT_LOG_LONG + +# set up global space, logging etc. +# set these true if you want them set globally for debugging +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_LOGLEVEL = int(env_bool(ENVAR_DCCSI_LOGLEVEL, int(20))) +if _DCCSI_GDEBUG: + _DCCSI_LOGLEVEL = int(10) + +_logging.basicConfig(format=FRMT_LOG_LONG, level=_DCCSI_LOGLEVEL) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -# settings.setenv() # doing this will add the additional DYNACONF_ envars -def get_dccsi_config(DCCSIG_PATH=_DCCSIG_PATH): +# _settings.setenv() # doing this will add the additional DYNACONF_ envars +def get_dccsi_config(DCCSI_PATH=_DCCSI_PATH): """Convenience method to set and retreive settings directly from module.""" # we can go ahead and just make sure the the DCCsi env is set - # config is SO generic this ensures we are importing a specific one - _spec_dccsi_config = importlib.util.spec_from_file_location("dccsi.config", - Path(DCCSIG_PATH, + # _config is SO generic this ensures we are importing a specific one + _spec_dccsi_config = importlib.util.spec_from_file_location("dccsi._config", + Path(DCCSI_PATH, "config.py")) _dccsi_config = importlib.util.module_from_spec(_spec_dccsi_config) _spec_dccsi_config.loader.exec_module(_dccsi_config) @@ -65,9 +89,12 @@ def get_dccsi_config(DCCSIG_PATH=_DCCSIG_PATH): return _dccsi_config # ------------------------------------------------------------------------- -# set and retreive the base settings on import -config = get_dccsi_config() -settings = config.get_config_settings() +# set and retreive the base env context/_settings on import +_config = get_dccsi_config() +_settings = _config.get_config_settings() + +if _DCCSI_DEV_MODE: + _config.attach_debugger() # attempts to start debugger # done with basic setup # --- END ----------------------------------------------------------------- @@ -77,50 +104,99 @@ settings = config.get_config_settings() # ------------------------------------------------------------------------- if __name__ == '__main__': """Run this file as main""" + + # ------------------------------------------------------------------------- + _O3DE_RUNNING=None + try: + import azlmbr + _O3DE_RUNNING=True + except: + _O3DE_RUNNING=False + # ------------------------------------------------------------------------- + + _MODULENAME = __name__ + if _MODULENAME is '__main__': + _MODULENAME = 'O3DE.DCCsi.bootstrap' + + from azpy.constants import STR_CROSSBAR - _G_DEBUG = False - _G_TEST_PYSIDE = False + # module internal debugging flags + while 0: # temp internal debug flag + _DCCSI_GDEBUG = True + break + + # overide logger for standalone to be more verbose and log to file + import azpy + _LOGGER = azpy.initialize_logger(_MODULENAME, + log_to_file=_DCCSI_GDEBUG, + default_log_level=_DCCSI_LOGLEVEL) + # happy print + _LOGGER.info(STR_CROSSBAR) + _LOGGER.info('~ constants.py ... Running script as __main__') + _LOGGER.info(STR_CROSSBAR) + + # parse the command line args + import argparse + parser = argparse.ArgumentParser( + description='O3DE DCCsi Boostrap (Test)', + epilog="Will externally test the DCCsi boostrap") _config = get_dccsi_config() - _settings = config.get_config_settings() - - _log_level = int(_settings.DCCSI_LOGLEVEL) - if _G_DEBUG: - _log_level = int(10) # force debug level - _LOGGER = azpy.initialize_logger(_MODULE, - log_to_file=True, - default_log_level=_log_level) + _settings = _config.get_config_settings(enable_o3de_python=True, + enable_o3de_pyside2=True) + parser.add_argument('-gd', '--global-debug', + type=bool, + required=False, + help='Enables global debug flag.') + parser.add_argument('-dm', '--developer-mode', + type=bool, + required=False, + help='Enables dev mode for early auto attaching debugger.') + parser.add_argument('-tp', '--test-pyside2', + type=bool, + required=False, + help='Runs Qt/PySide2 tests and reports.') + args = parser.parse_args() + + # easy overrides + if args.global_debug: + _DCCSI_GDEBUG = True + if args.developer_mode: + _DCCSI_DEV_MODE = True + _config.attach_debugger() # attempts to start debugger - # we can now grab values from the DCCsi.config.py dynamic env settings - # the rest of this block is basic debug testing the dynamic settings at boot - _LOGGER.info(f'Running module: {_MODULE}') - _LOGGER.info(f'DCCSIG_PATH: {_settings.DCCSIG_PATH}') - _LOGGER.info(f'DCCSI_G_DEBUG: {_settings.DCCSI_GDEBUG}') - _LOGGER.info(f'DCCSI_DEV_MODE: {_settings.DCCSI_DEV_MODE}') - - _LOGGER.info(f'OS_FOLDER: {_settings.OS_FOLDER}') - _LOGGER.info(f'LY_PROJECT: {_settings.LY_PROJECT}') - _LOGGER.info(f'LY_PROJECT_PATH: {_settings.LY_PROJECT_PATH}') - _LOGGER.info(f'LY_DEV: {_settings.LY_DEV}') - _LOGGER.info(f'LY_BUILD_PATH: {_settings.LY_BUILD_PATH}') - _LOGGER.info(f'LY_BIN_PATH: {_settings.LY_BIN_PATH}') + if _DCCSI_GDEBUG: + _LOGGER.info(f'DCCSI_PATH: {_settings.DCCSI_PATH}') + _LOGGER.info(f'DCCSI_G_DEBUG: {_settings.DCCSI_GDEBUG}') + _LOGGER.info(f'DCCSI_DEV_MODE: {_settings.DCCSI_DEV_MODE}') - _LOGGER.info(f'DCCSIG_PATH: {_settings.DCCSIG_PATH}') - _LOGGER.info(f'DCCSI_PYTHON_LIB_PATH: {_settings.DCCSI_PYTHON_LIB_PATH}') - _LOGGER.info(f'DDCCSI_PY_BASE: {_settings.DDCCSI_PY_BASE}') + _LOGGER.info(f'DCCSI_OS_FOLDER: {_settings.DCCSI_OS_FOLDER}') + _LOGGER.info(f'O3DE_PROJECT: {_settings.O3DE_PROJECT}') + _LOGGER.info(f'O3DE_PROJECT_PATH: {_settings.O3DE_PROJECT_PATH}') + _LOGGER.info(f'O3DE_DEV: {_settings.O3DE_DEV}') + _LOGGER.info(f'O3DE_BUILD_PATH: {_settings.O3DE_BUILD_PATH}') + _LOGGER.info(f'O3DE_BIN_PATH: {_settings.O3DE_BIN_PATH}') + + _LOGGER.info(f'DCCSI_PATH: {_settings.DCCSI_PATH}') + _LOGGER.info(f'DCCSI_PYTHON_LIB_PATH: {_settings.DCCSI_PYTHON_LIB_PATH}') + _LOGGER.info(f'DCCSI_PY_BASE: {_settings.DCCSI_PY_BASE}') - if _G_TEST_PYSIDE: + if _DCCSI_GDEBUG or args.test_pyside2: try: import PySide2 except: # set up Qt/PySide2 access and test - _settings = _config.get_config_settings(setup_ly_pyside=True) + _settings = _config.get_config_settings(enable_o3de_pyside2=True) import PySide2 _LOGGER.info(f'PySide2: {PySide2}') - _LOGGER.info(f'LY_BIN_PATH: {_settings.LY_BIN_PATH}') + _LOGGER.info(f'O3DE_BIN_PATH: {_settings.O3DE_BIN_PATH}') _LOGGER.info(f'QT_PLUGIN_PATH: {_settings.QT_PLUGIN_PATH}') _LOGGER.info(f'QT_QPA_PLATFORM_PLUGIN_PATH: {_settings.QT_QPA_PLATFORM_PLUGIN_PATH}') _config.test_pyside2() + + if not _O3DE_RUNNING: + # return + sys.exit() # --- END ----------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/README.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/README.txt new file mode 100644 index 0000000000..9a3efe589a --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/README.txt @@ -0,0 +1,87 @@ +""" +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 folder contains the DccScriptingInterface (DCCsi) for O3DE + +Notice: The old \\SDK folder is being replaced with \\Tools +The scripts in \\SDK may be out of data (and not run) +When scripts are finished being updated and refactored into \\Tools the \\SDK will be removed + +What is the DCCsi? + +- A shared development environment for technical art oriented to working with Python across a number of DCC tools. +- Leverage the existing python ecosystem for technical art. +- Integrate a DCC app like Substance (or Substance SAT api) from the Python driven VFX and Games ecosystem. +- Extend O3DE and unlock its potential for content creators, and the Technical Artists that service them. + +Tenets: +(1) Interoperability: Design DCC-agnostic modules and DCC-bespoke modules + to work together efficiently and intuitively. + +(2) Encapsulation: Define a module in terms of its essential features and interface to other components, + to facilitate logical layered design and easy maintenance. + +(3) Extensibility: Design the tool set to be easily extensible with new functionality and new tools. + Individual pieces should have a generic communication mechanism to allow newly written tools to slot cleanly and transparently into the tool chain. + +What is provided (High Level): +- DCC-Agnostic Python Framework (as a modular Gem) related to multiple integrations for: + O3DE Editor (python scripting, utils and PySide2 tools) + DCC applications and their Python APIs/SDKs + Custom standalone tools and utils (python based) + external from cmd line + external standalone + integrated to run within O3DE Editor + +What is provided (by folder): + +\3rdParty: Allows third party libs/packages to be integrated outside of O3DE + Example: O3DE is py3, Maya 2020 (and earlier) is py27 + O3DE provides a patterns for Gems to provide a requirements.txt + See: + DccScriptingInterface\reqiurements.txt + ^ These packages will be fetched and installed into O3DE python at build time + + This means for some applications like Maya we need another way to add the same packages + See: + DccScriptingInterface\SDK\Maya\readme.txt + DccScriptingInterface\SDK\Maya\requirements.txt + DccScriptingInterface\3rdParty\Python\Lib\2.x\2.7.x\site-packages\* + + Packages that reside in 3rdParty are never commited to the repo (only fetched+installed) + +\Assets: All O3DE Gems can maintain an asset folder + If a Gem contains an \Asset folder, these assets are folded into the projects asset data + These assets are processed by the Asset Processor for use in the Editor and Runtime + In the DCCsi the \Assets folder primarily contains TestData + +\azpy Core (shared) API, A pure python Package and Modules + +\Code Contains the bare bones C++ scaffold to build and integrate the Gem with O3DE + Notes: portions of the DCCsi can be utilized outside of O3DE + thus this Gem doens't have to be enabled and built for some use cases + +\Editor This folder provides an entry point pattern for extending O3DE Editor with python + When a Gem is enabled ... + If the following if found, it will be executed when the Editor boots: + "Editor\Scripts\bootstrap.py" + + This can be used to initialize code access, extend the editor (PySide2), etc. + +\Tools This is where the following is maintained: + +\Tools\DCC Integration for DCC tools: + configuration of tool (managed env, etc.) + bootstrapping, such as providing the tool access to azpy api code + extensibility, such as adding new functionality or tool to the app + +\Tools\DCC\Maya An example of adding a integration for Autodesk Maya + +\Tools\Env\Windows This provides a .bat file managed env to configure and bootsrap windows apps +\Tools\Launchers\windows Provides .bat files based tool launchers for windows (accesses env) + diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Atom/Scripts/Python/minspect.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Atom/Scripts/Python/minspect.py deleted file mode 100644 index 760807042c..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Atom/Scripts/Python/minspect.py +++ /dev/null @@ -1,132 +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 pymel.core as pmc -import sys -import types - - -def syspath(): - print 'sys.path:' - for p in sys.path: - print ' ' + p - - -def info(obj): - """Prints information about the object.""" - - lines = ['Info for %s' % obj.name(), - 'Attributes:'] - # Get the name of all attributes - for a in obj.listAttr(): - lines.append(' ' + a.name()) - lines.append('MEL type: %s' % obj.type()) - lines.append('MRO:') - lines.extend([' ' + t.__name__ for t in type(obj).__mro__]) - result = '\n'.join(lines) - print result - - -def _is_pymel(obj): - try: # (1) - module = obj.__module__ # (2) - except AttributeError: # (3) - try: - module = obj.__name__ # (4) - except AttributeError: - return None # (5) - return module.startswith('pymel') # (6) - - -def _py_to_helpstr(obj): - if isinstance(obj, basestring): - return 'search.html?q=%s' % (obj.replace(' ', '+')) - if not _is_pymel(obj): - return None - if isinstance(obj, types.ModuleType): - return ('generated/%(module)s.html#module-%(module)s' % - dict(module=obj.__name__)) - if isinstance(obj, types.MethodType): - return ('generated/classes/%(module)s/' - '%(module)s.%(typename)s.html' - '#%(module)s.%(typename)s.%(methname)s' % dict( - module=obj.__module__, - typename=obj.im_class.__name__, - methname=obj.__name__)) - if isinstance(obj, types.FunctionType): - return ('generated/functions/%(module)s/' - '%(module)s.%(funcname)s.html' - '#%(module)s.%(funcname)s' % dict( - module=obj.__module__, - funcname=obj.__name__)) - if not isinstance(obj, type): - obj = type(obj) - return ('generated/classes/%(module)s/' - '%(module)s.%(typename)s.html' - '#%(module)s.%(typename)s' % dict( - module=obj.__module__, - typename=obj.__name__)) - - -def test_py_to_helpstr(): - def dotest(obj, ideal): - result = _py_to_helpstr(obj) - assert result == ideal, '%s != %s' % (result, ideal) - dotest('maya rocks', 'search.html?q=maya+rocks') - dotest(pmc.nodetypes, - 'generated/pymel.core.nodetypes.html' - '#module-pymel.core.nodetypes') - dotest(pmc.nodetypes.Joint, - 'generated/classes/pymel.core.nodetypes/' - 'pymel.core.nodetypes.Joint.html' - '#pymel.core.nodetypes.Joint') - dotest(pmc.nodetypes.Joint(), - 'generated/classes/pymel.core.nodetypes/' - 'pymel.core.nodetypes.Joint.html' - '#pymel.core.nodetypes.Joint') - dotest(pmc.nodetypes.Joint().getTranslation, - 'generated/classes/pymel.core.nodetypes/' - 'pymel.core.nodetypes.Joint.html' - '#pymel.core.nodetypes.Joint.getTranslation') - dotest(pmc.joint, - 'generated/functions/pymel.core.animation/' - 'pymel.core.animation.joint.html' - '#pymel.core.animation.joint') - dotest(object(), None) - dotest(10, None) - dotest([], None) - dotest(sys, None) - - -def test_py_to_helpstrFAIL(): - assert 1 == 2, '1 != 2' - - -import webbrowser # (1) -HELP_ROOT_URL = ('http://help.autodesk.com/cloudhelp/2018/ENU/Maya-Tech-Docs/PyMel/')# (2) - - -def pmhelp(obj): # (3) - """Gives help for a pymel or python object. - - If obj is not a PyMEL object, use Python's built-in - `help` function. - If obj is a string, open a web browser to a search in the - PyMEL help for the string. - Otherwise, open a web browser to the page for the object. - """ - tail = _py_to_helpstr(obj) - if tail is None: - help(obj) # (4) - else: - webbrowser.open(HELP_ROOT_URL + tail) # (5) - - -if __name__ == '__main__': - test_py_to_helpstr() - print 'Tests ran successfully.' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/kitbash_converter/launcher.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/kitbash_converter/launcher.bat deleted file mode 100644 index fb07178344..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/kitbash_converter/launcher.bat +++ /dev/null @@ -1,85 +0,0 @@ -@echo off - -REM -REM Copyright (c) Contributors to the Open 3D Engine Project. -REM For complete copyright and license terms please see the LICENSE at the root of this distribution. -REM -REM SPDX-License-Identifier: Apache-2.0 OR MIT -REM -REM - -:: Set up and run LY Python CMD prompt -:: Sets up the DccScriptingInterface_Env, -:: Puts you in the CMD within the dev environment - -:: Set up window -TITLE Lumberyard DCC Scripting Interface Cmd -:: Use obvious color to prevent confusion (Grey with Yellow Text) -COLOR 8E - -%~d0 -cd %~dp0 - -:: Keep changes local -SETLOCAL enableDelayedExpansion - -:: This maps up to the \Dev folder -IF "%DEV_REL_PATH%"=="" (set DEV_REL_PATH=..\..\..\..\..\..\..\..\..) - -:: Change to root Lumberyard dev dir -:: Don't use the LY_DEV so we can test that ENVAR!!! -CD /d %DEV_REL_PATH% -set Rel_Dev=%CD% -echo Rel_Dev = %Rel_Dev% -:: Restore original directory -popd - -set DCCSI_PYTHON_INSTALL=%Rel_Dev%\Tools\Python\3.7.5\windows - -:: add to the PATH -SET PATH=%DCCSI_PYTHON_INSTALL%;%PATH% - -:: dcc scripting interface gem path -set DCCSIG_PATH=%Rel_Dev%\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface -echo DCCSIG_PATH = %DCCSIG_PATH% - -:: add to the PATH -SET PATH=%DCCSIG_PATH%;%PATH% - -:: Constant Vars (Global) -:: global debug (propogates) -IF "%DCCSI_GDEBUG%"=="" (set DCCSI_GDEBUG=false) -echo DCCSI_GDEBUG = %DCCSI_GDEBUG% -:: initiates debugger connection -IF "%DCCSI_DEV_MODE%"=="" (set DCCSI_DEV_MODE=false) -echo DCCSI_DEV_MODE = %DCCSI_DEV_MODE% -:: sets debugger, options: WING, PYCHARM -IF "%DCCSI_GDEBUGGER%"=="" (set DCCSI_GDEBUGGER=WING) -echo DCCSI_GDEBUGGER = %DCCSI_GDEBUGGER% - -echo. -echo _____________________________________________________________________ -echo. -echo ~ LY DCCsi, DCC Material Converter -echo _____________________________________________________________________ -echo. - -:: Change to root dir -CD /D %DCCSIG_PATH% - -:: add to the PATH -SET PATH=%DCCSIG_PATH%;%PATH% - -set PYTHONPATH=%DCCSIG_PATH%;%PYTHONPATH% - -CALL %DCCSI_PYTHON_INSTALL%\python.exe "%DCCSIG_PATH%\SDK\Maya\Scripts\Python\kitbash_converter\standalone.py" - - -ENDLOCAL - -:: Return to starting directory -POPD - -:END_OF_FILE - -exit /b 0 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/kitbash_converter/main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/kitbash_converter/main.py index e554b1ca68..d0e9759208 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/kitbash_converter/main.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/kitbash_converter/main.py @@ -51,8 +51,8 @@ module_name = 'kitbash_converter.main' log_file_path = os.path.join(settings.DCCSI_LOG_PATH, f'{module_name}.log') _log_level = int(20) -_G_DEBUG = True -if _G_DEBUG: +_DCCSI_GDEBUG = True +if _DCCSI_GDEBUG: _log_level = int(10) from azpy.constants import FRMT_LOG_LONG diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_Cmd.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_Cmd.bat deleted file mode 100644 index ce118d7710..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_Cmd.bat +++ /dev/null @@ -1,45 +0,0 @@ -:: coding:utf-8 -:: !/usr/bin/python -:: -:: 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 -:: -:: - -@echo off -:: Set up and run LY Python CMD prompt -:: Sets up the DccScriptingInterface_Env, -:: Puts you in the CMD within the dev environment - -:: Set up window -TITLE Lumberyard DCC Scripting Interface Cmd -:: Use obvious color to prevent confusion (Grey with Yellow Text) -COLOR 8E - -%~d0 -cd %~dp0 -PUSHD %~dp0 - -:: Keep changes local -SETLOCAL enableDelayedExpansion - -CALL %~dp0\Project_Env.bat - -echo. -echo _____________________________________________________________________ -echo. -echo ~ LY DCC Scripting Interface CMD ... -echo _____________________________________________________________________ -echo. - -:: Create command prompt with environment -CALL %windir%\system32\cmd.exe - -ENDLOCAL - -:: Return to starting directory -POPD - -:END_OF_FILE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_Maya_2020.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_Maya_2020.bat deleted file mode 100644 index 32e910790e..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_Maya_2020.bat +++ /dev/null @@ -1,68 +0,0 @@ -:: coding:utf-8 -:: !/usr/bin/python -:: -:: 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 -:: -:: - -@echo off -:: Launches maya with a bunch of local hooks for Lumberyard -:: ToDo: move all of this to a .json data driven boostrapping system - -%~d0 -cd %~dp0 -PUSHD %~dp0 - -echo ________________________________ -echo ~ calling PROJ_Env.bat - -:: Keep changes local -SETLOCAL enableDelayedExpansion - -:: PY version Major -set DCCSI_PY_VERSION_MAJOR=2 -echo DCCSI_PY_VERSION_MAJOR = %DCCSI_PY_VERSION_MAJOR% - -:: PY version Major -set DCCSI_PY_VERSION_MINOR=7 -echo DCCSI_PY_VERSION_MINOR = %DCCSI_PY_VERSION_MINOR% - -:: Maya Version -set MAYA_VERSION=2020 -echo MAYA_VERSION = %MAYA_VERSION% - -:: if a local customEnv.bat exists, run it -IF EXIST "%~dp0Project_Env.bat" CALL %~dp0Project_Env.bat - -echo ________________________________ -echo Launching Maya %MAYA_VERSION% for Lumberyard... - -:::: Set Maya native project acess to this project -::set MAYA_PROJECT=%LY_PROJECT% -::echo MAYA_PROJECT = %MAYA_PROJECT% - -:: DX11 Viewport -Set MAYA_VP2_DEVICE_OVERRIDE = VirtualDeviceDx11 - -:: Default to the right version of Maya if we can detect it... and launch -IF EXIST "%MAYA_LOCATION%\bin\Maya.exe" ( - start "" "%MAYA_LOCATION%\bin\Maya.exe" %* -) ELSE ( - Where maya.exe 2> NUL - IF ERRORLEVEL 1 ( - echo Maya.exe could not be found - pause - ) ELSE ( - start "" Maya.exe %* - ) -) - -:: Return to starting directory -POPD - -:END_OF_FILE - -exit /b 0 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_WingIDE-7-1.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_WingIDE-7-1.bat deleted file mode 100644 index 4d82bd3e16..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_WingIDE-7-1.bat +++ /dev/null @@ -1,81 +0,0 @@ -:: coding:utf-8 -:: !/usr/bin/python -:: -:: 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 -:: -:: - -@echo off -:: Launches Wing IDE and the DccScriptingInterface Project Files - -echo. -echo _____________________________________________________________________ -echo. -echo ~ Setting up LY DCCsi WingIDE Dev Env... -echo _____________________________________________________________________ -echo. - -:: Store current dir -%~d0 -cd %~dp0 -PUSHD %~dp0 - -:: Keep changes local -SETLOCAL enableDelayedExpansion - -SET ABS_PATH=%~dp0 -echo Current Dir, %ABS_PATH% - -:: WingIDE version Major -SET WING_VERSION_MAJOR=7 -echo WING_VERSION_MAJOR = %WING_VERSION_MAJOR% - -:: WingIDE version Major -SET WING_VERSION_MINOR=1 -echo WING_VERSION_MINOR = %WING_VERSION_MINOR% - -:: note the changed path from IDE to Pro -set WINGHOME=%PROGRAMFILES(X86)%\Wing Pro %WING_VERSION_MAJOR%.%WING_VERSION_MINOR% -echo WINGHOME = %WINGHOME% - -CALL %~dp0\Project_Env.bat - -echo. -echo _____________________________________________________________________ -echo. -echo ~ WingIDE Version %WING_VERSION_MAJOR%.%WING_VERSION_MINOR% -echo _____________________________________________________________________ -echo. - -SET WING_PROJ=%DCCSIG_PATH%\Solutions\.wing\DCCsi_%WING_VERSION_MAJOR%x.wpr -echo WING_PROJ = %WING_PROJ% - -echo. -echo _____________________________________________________________________ -echo. -echo ~ Launching %LY_PROJECT% project in WingIDE %WING_VERSION_MAJOR%.%WING_VERSION_MINOR% ... -echo _____________________________________________________________________ -echo. - - -IF EXIST "%WINGHOME%\bin\wing.exe" ( - start "" "%WINGHOME%\bin\wing.exe" "%WING_PROJ%" -) ELSE ( - Where wing.exe 2> NUL - IF ERRORLEVEL 1 ( - echo wing.exe could not be found - pause - ) ELSE ( - start "" wing.exe "%WING_PROJ%" - ) -) - -ENDLOCAL - -:: Return to starting directory -POPD - -:END_OF_FILE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Project_Env.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Project_Env.bat deleted file mode 100644 index f958d98616..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Project_Env.bat +++ /dev/null @@ -1,72 +0,0 @@ -:: coding:utf-8 -:: !/usr/bin/python -:: -:: 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 -:: -:: - -@echo off -:: Sets up environment for Lumberyard DCC tools and code access - -:: Store current dir -%~d0 -cd %~dp0 -PUSHD %~dp0 - -for %%a in (.) do set LY_PROJECT=%%~na - -echo. -echo _____________________________________________________________________ -echo. -echo ~ Setting up LY DSI PROJECT Environment ... -echo _____________________________________________________________________ -echo. - -echo LY_PROJECT = %LY_PROJECT% - -:: Put you project env vars and overrides here - -:: chanhe the relative path up to dev -set DEV_REL_PATH=../../.. -set ABS_PATH=%~dp0 - -:: Override the default maya version -set MAYA_VERSION=2020 -echo MAYA_VERSION = %MAYA_VERSION% - -set LY_PROJECT_PATH=%ABS_PATH% -echo LY_PROJECT_PATH = %LY_PROJECT_PATH% - -:: Change to root Lumberyard dev dir -CD /d %LY_PROJECT_PATH%\%DEV_REL_PATH% -set LY_DEV=%CD% -echo LY_DEV = %LY_DEV% - -CALL %LY_DEV%\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\Launchers\Windows\Env.bat - -rem :: Constant Vars (Global) -rem SET LYPY_GDEBUG=0 -rem echo LYPY_GDEBUG = %LYPY_GDEBUG% -rem SET LYPY_DEV_MODE=0 -rem echo LYPY_DEV_MODE = %LYPY_DEV_MODE% -rem SET LYPY_DEBUGGER=WING -rem echo LYPY_DEBUGGER = %LYPY_DEBUGGER% - -:: Restore original directory -popd - -:: Change to root dir -CD /D %ABS_PATH% - -:: if the user has set up a custom env call it -IF EXIST "%~dp0User_Env.bat" CALL %~dp0User_Env.bat - -GOTO END_OF_FILE - -:: Return to starting directory -POPD - -:END_OF_FILE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/main.py index 3b985199bb..09caef836f 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/main.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/main.py @@ -80,8 +80,8 @@ module_name = 'legacy_asset_converter.main' log_file_path = os.path.join(settings.DCCSI_LOG_PATH, f'{module_name}.log') _log_level = int(20) -_G_DEBUG = True -if _G_DEBUG: +_DCCSI_GDEBUG = True +if _DCCSI_GDEBUG: _log_level = int(10) from azpy.constants import FRMT_LOG_LONG diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/maya_dcc_materials/minspect.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/maya_dcc_materials/minspect.py deleted file mode 100644 index 668382480c..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/maya_dcc_materials/minspect.py +++ /dev/null @@ -1,135 +0,0 @@ -# -*- coding: utf-8 -*- -# !/usr/bin/python -# -# 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 pymel.core as pmc -import sys -import types - - -def syspath(): - print 'sys.path:' - for p in sys.path: - print ' ' + p - - -def info(obj): - """Prints information about the object.""" - - lines = ['Info for %s' % obj.name(), - 'Attributes:'] - # Get the name of all attributes - for a in obj.listAttr(): - lines.append(' ' + a.name()) - lines.append('MEL type: %s' % obj.type()) - lines.append('MRO:') - lines.extend([' ' + t.__name__ for t in type(obj).__mro__]) - result = '\n'.join(lines) - print result - - -def _is_pymel(obj): - try: # (1) - module = obj.__module__ # (2) - except AttributeError: # (3) - try: - module = obj.__name__ # (4) - except AttributeError: - return None # (5) - return module.startswith('pymel') # (6) - - -def _py_to_helpstr(obj): - if isinstance(obj, basestring): - return 'search.html?q=%s' % (obj.replace(' ', '+')) - if not _is_pymel(obj): - return None - if isinstance(obj, types.ModuleType): - return ('generated/%(module)s.html#module-%(module)s' % - dict(module=obj.__name__)) - if isinstance(obj, types.MethodType): - return ('generated/classes/%(module)s/' - '%(module)s.%(typename)s.html' - '#%(module)s.%(typename)s.%(methname)s' % dict( - module=obj.__module__, - typename=obj.im_class.__name__, - methname=obj.__name__)) - if isinstance(obj, types.FunctionType): - return ('generated/functions/%(module)s/' - '%(module)s.%(funcname)s.html' - '#%(module)s.%(funcname)s' % dict( - module=obj.__module__, - funcname=obj.__name__)) - if not isinstance(obj, type): - obj = type(obj) - return ('generated/classes/%(module)s/' - '%(module)s.%(typename)s.html' - '#%(module)s.%(typename)s' % dict( - module=obj.__module__, - typename=obj.__name__)) - - -def test_py_to_helpstr(): - def dotest(obj, ideal): - result = _py_to_helpstr(obj) - assert result == ideal, '%s != %s' % (result, ideal) - dotest('maya rocks', 'search.html?q=maya+rocks') - dotest(pmc.nodetypes, - 'generated/pymel.core.nodetypes.html' - '#module-pymel.core.nodetypes') - dotest(pmc.nodetypes.Joint, - 'generated/classes/pymel.core.nodetypes/' - 'pymel.core.nodetypes.Joint.html' - '#pymel.core.nodetypes.Joint') - dotest(pmc.nodetypes.Joint(), - 'generated/classes/pymel.core.nodetypes/' - 'pymel.core.nodetypes.Joint.html' - '#pymel.core.nodetypes.Joint') - dotest(pmc.nodetypes.Joint().getTranslation, - 'generated/classes/pymel.core.nodetypes/' - 'pymel.core.nodetypes.Joint.html' - '#pymel.core.nodetypes.Joint.getTranslation') - dotest(pmc.joint, - 'generated/functions/pymel.core.animation/' - 'pymel.core.animation.joint.html' - '#pymel.core.animation.joint') - dotest(object(), None) - dotest(10, None) - dotest([], None) - dotest(sys, None) - - -def test_py_to_helpstrFAIL(): - assert 1 == 2, '1 != 2' - - -import webbrowser # (1) -HELP_ROOT_URL = ('http://help.autodesk.com/cloudhelp/2018/ENU/Maya-Tech-Docs/PyMel/')# (2) - - -def pmhelp(obj): # (3) - """Gives help for a pymel or python object. - - If obj is not a PyMEL object, use Python's built-in - `help` function. - If obj is a string, open a web browser to a search in the - PyMEL help for the string. - Otherwise, open a web browser to the page for the object. - """ - tail = _py_to_helpstr(obj) - if tail is None: - help(obj) # (4) - else: - webbrowser.open(HELP_ROOT_URL + tail) # (5) - - -if __name__ == '__main__': - test_py_to_helpstr() - print 'Tests ran successfully.' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter.py index 10cdac813a..4c24a61b59 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter.py @@ -23,7 +23,7 @@ def returnStubDir(stub): break if (len(tail) == 0): path = "" - if _G_DEBUG: + if _DCCSI_GDEBUG: print('~ Debug Message: I was not able to find the ' 'path to that file (stub) in a walk-up from currnet path') break diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter_maya.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter_maya.py index 3ea7b1e5e7..3a7ec49e74 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter_maya.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter_maya.py @@ -24,7 +24,7 @@ def returnStubDir(stub, start_path): break if (len(tail) == 0): path = "" - if _G_DEBUG: + if _DCCSI_GDEBUG: print('~ Debug Message: I was not able to find the ' 'path to that file (stub) in a walk-up from currnet path') break diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_callbacks.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_callbacks.py index 4dda18c403..8c3b0f6a64 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_callbacks.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_callbacks.py @@ -42,7 +42,7 @@ from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, True) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, True) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, True) _MODULENAME = r'DCCsi.SDK.Maya.Scripts.set_callbacks' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_defaults.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_defaults.py index d6db5e42f4..639b03c297 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_defaults.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_defaults.py @@ -37,7 +37,7 @@ from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _MODULENAME = r'DCCsi.SDK.Maya.Scripts.set_defaults' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_menu.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_menu.py index 4e85a448c1..e417324d0b 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_menu.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_menu.py @@ -37,7 +37,7 @@ from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _MODULENAME = r'DCCsi.SDK.Maya.Scripts.set_menu' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/userSetup.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/userSetup.py index aee5563c3d..04eb027142 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/userSetup.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/userSetup.py @@ -56,7 +56,7 @@ import maya.mel as mel # ------------------------------------------------------------------------- # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) #_DCCSI_DEV_MODE = True # force true for debugger testing @@ -69,7 +69,7 @@ _MODULENAME = str('{0}.{1}'.format(_APP_TAG, _TOOL_TAG)) _LOGGER = azpy.initialize_logger(_MODULENAME, default_log_level=int(20)) _LOGGER.info('Initializing: {0}.'.format({_MODULENAME})) -_LOGGER.info('DCCSI_GDEBUG: {0}.'.format({_G_DEBUG})) +_LOGGER.info('DCCSI_GDEBUG: {0}.'.format({_DCCSI_GDEBUG})) _LOGGER.info('DCCSI_DEV_MODE: {0}.'.format({_DCCSI_DEV_MODE})) # flag to turn off setting up callbacks, until they are fully implemented @@ -175,17 +175,17 @@ try: except Exception as e: _LOGGER.critical(_STR_ERROR_ENVAR.format(_BASE_ENVVAR_DICT[ENVAR_DCCSI_SDK_PATH])) -_LY_PROJECT_PATH = None +_O3DE_PROJECT_PATH = None try: - _LY_PROJECT_PATH = _BASE_ENVVAR_DICT[ENVAR_LY_PROJECT_PATH] + _O3DE_PROJECT_PATH = _BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT_PATH] except Exception as e: - _LOGGER.critical(_STR_ERROR_ENVAR.format(_BASE_ENVVAR_DICT[ENVAR_LY_PROJECT_PATH])) + _LOGGER.critical(_STR_ERROR_ENVAR.format(_BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT_PATH])) # check some env var tags (fail if no, likely means no proper code access) -_LY_DEV = _BASE_ENVVAR_DICT[ENVAR_LY_DEV] -_LY_DCCSIG_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSIG_PATH] -_LY_DCCSI_LOG_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSI_LOG_PATH] -_LY_AZPY_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSI_AZPY_PATH] +_O3DE_DEV = _BASE_ENVVAR_DICT[ENVAR_O3DE_DEV] +_O3DE_DCCSIG_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSIG_PATH] +_O3DE_DCCSI_LOG_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSI_LOG_PATH] +_O3DE_AZPY_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSI_AZPY_PATH] # ------------------------------------------------------------------------- @@ -270,18 +270,18 @@ def post_startup(): install_fix_paths() # set the project workspace - #_LY_PROJECT_PATH = _BASE_ENVVAR_DICT[ENVAR_LY_PROJECT_PATH] - _project_workspace = os.path.join(_LY_PROJECT_PATH, TAG_MAYA_WORKSPACE) + #_O3DE_PROJECT_PATH = _BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT_PATH] + _project_workspace = os.path.join(_O3DE_PROJECT_PATH, TAG_MAYA_WORKSPACE) if os.path.isfile(_project_workspace): try: # load workspace - maya.cmds.workspace(_LY_PROJECT_PATH, openWorkspace=True) + maya.cmds.workspace(_O3DE_PROJECT_PATH, openWorkspace=True) _LOGGER.info('Loaded workspace file: {0}'.format(_project_workspace)) - maya.cmds.workspace(_LY_PROJECT_PATH, update=True) + maya.cmds.workspace(_O3DE_PROJECT_PATH, update=True) except Exception as e: _LOGGER.error(e) else: - _LOGGER.warning('Workspace file not found: {1}'.format(_LY_PROJECT_PATH)) + _LOGGER.warning('Workspace file not found: {1}'.format(_O3DE_PROJECT_PATH)) # Set up Lumberyard, maya default setting from set_defaults import set_defaults @@ -292,7 +292,7 @@ def post_startup(): _LOGGER.info('Add UI dependent tools') # wrap in a try, because we haven't implmented it yet try: - mel.eval(str(r'source "{}"'.format(TAG_LY_DCC_MAYA_MEL))) + mel.eval(str(r'source "{}"'.format(TAG_O3DE_DCC_MAYA_MEL))) except Exception as e: _LOGGER.error(e) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/blender_materials.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/blender_materials.py deleted file mode 100755 index 6d7bd10f89..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/blender_materials.py +++ /dev/null @@ -1,85 +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 bpy -import collections -import json - - -def get_shader_information(): - """ - Queries all materials and corresponding material attributes and file textures in the Blender scene. - - :return: - """ - # TODO - link file texture location to PBR material plugs- finding it difficult to track down how this is achieved - # in the Blender Python API documentation and/or in forums - - materials_count = 1 - shader_types = get_blender_shader_types() - materials_dictionary = {} - for target_mesh in [o for o in bpy.data.objects if type(o.data) is bpy.types.Mesh]: - material_information = collections.OrderedDict(DccApplication='Blender', AppliedMesh=target_mesh, - SceneName=bpy.data.filepath, MaterialAttributes={}, - FileConnections={}) - for target_material in target_mesh.data.materials: - material_information['MaterialName'] = target_material.name - shader_attributes = {} - shader_file_connections = {} - - for node in target_material.node_tree.nodes: - socket = node.inputs[0] - print('NODE: {}'.format(node)) - print('Socket: {}'.format(socket)) - - for material_input in node.inputs: - attribute_name = material_input.name - try: - attribute_value = material_input.default_value - print('Name: [{}] [{}] ValueType ::::::> {}'.format(attribute_name, attribute_value, - type(attribute_value))) - material_information['MaterialAttributes'].update({attribute_name: str(attribute_value)}) - except Exception as e: - pass - print('\n') - if node.type == 'TEX_IMAGE': - material_information['FileConnections'].update({str(node): str(node.image.filepath)}) - if node.name in shader_types.keys(): - material_information['MaterialType'] = shader_types[node.name] - - -# material_information['MaterialAttributes'] = shader_attributes - materials_dictionary['Material_{}'.format(materials_count)] = material_information - materials_count += 1 - print('_________________________________________________________________\n') - - return materials_dictionary - - -def get_blender_shader_types(): - """ - This returns all the material types present in the Blender scene - :return: - """ - shader_types = {} - ddir = lambda data, filter_str: [i for i in dir(data) if i.startswith(filter_str)] - get_nodes = lambda cat: [i for i in getattr(bpy.types, cat).category.items(None)] - cycles_categories = ddir(bpy.types, "NODE_MT_category_SH_NEW") - for cat in cycles_categories: - if cat == 'NODE_MT_category_SH_NEW_SHADER': - for node in get_nodes(cat): - shader_types[node.label] = node.nodetype - return shader_types - - -materials_dictionary = get_shader_information() -#print('Materials Dictionary:') -#print(materials_dictionary) -#parsed = json.loads(str(materials_dictionary)) -#print(json.dumps(parsed, indent=4, sort_keys=True)) - - diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/cli_control.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/cli_control.py deleted file mode 100755 index 1d91275545..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/cli_control.py +++ /dev/null @@ -1,52 +0,0 @@ -# coding:utf-8 -#!/usr/bin/python -# -# 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 line is 75 characters ------------------------------------------- - -import click -import os -import main as app_main - - -@click.version_option('1.0.0') -@click.option('--output', default='PBR', help='Lumberyard material type. Current options: [pbr_basic]') -@click.argument('operands', type=click.STRING, nargs=-1) -@click.command(context_settings=dict(ignore_unknown_options=True)) -def main(output, operands): - target_files = [] - for index, operand in enumerate(operands): - entry_path = os.path.abspath(str(operand)) - if os.path.isdir(entry_path): - for directory_path, directory_names, file_names in os.walk(entry_path): - for file_name in file_names: - if is_valid_file(file_name): - target_files.append(os.path.join(entry_path, file_name)) - else: - if is_valid_file(operand): - target_files.append(operand) - - if len(target_files): - app_main.launch_material_converter('standalone', output, target_files) - - -def is_valid_file(file_name): - """ - Allows only supported DCC application files by extensions - :param file_name: The name of the file. - :return: - """ - target_extensions = 'ma mb fbx blend max'.split(' ') - if file_name.split('.')[-1] in target_extensions: - return True - return False - - -if __name__ == '__main__': - main() - diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/dcc_material_mapping.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/dcc_material_mapping.py deleted file mode 100755 index 6369cb5658..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/dcc_material_mapping.py +++ /dev/null @@ -1,65 +0,0 @@ -# coding:utf-8 -#!/usr/bin/python -# -# 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 line is 75 characters ------------------------------------------- - -import logging - -logging.basicConfig(level=logging.DEBUG) - -def get_maya_material_mapping(name, material_type, file_connections): - """ - Helps map found material DCC attribute values/file connections with Lumberyard materials. - - :param name: Material name from within Maya - :param material_type: Maya Material type to match values to (i.e. Stingray PBS, aiStandardSurface(Arnold) - :param file_connections: List of all connected texture files from Maya - :return: Key value pairs for attributes/file textures assigned as Lumberyard material values - """ - material_properties = {} - if material_type == 'StingrayPBS': - logging.debug('Mapping StingrayPBS') - maps = 'color, metallic, roughness, normal, emissive, ao, opacity'.split(', ') - naming_exceptions = {'color': 'baseColor', 'ao': 'ambientOcclusion'} - for m in maps: - texture_attribute = 'TEX_{}_map'.format(m) - for tex in file_connections.keys(): - if tex.find(texture_attribute) != -1: - key = m if m not in naming_exceptions else naming_exceptions.get(m) - logging.debug('Key, Value: {} {}.{}'.format(key, name, texture_attribute)) - material_properties[key] = {'useTexture': 'true', - 'textureMap': file_connections.get( - '{}.{}'.format(name, texture_attribute))} - elif material_type == 'aiStandardSurface': - logging.debug('Mapping AiStandardSurface') - # TODO- Occlusion is based on a more difficult setup- there is no standard channel. Set this up as time permits - maps = 'baseColor, metalness, specularRoughness, normal, emissionColor, opacity'.split(', ') - naming_exceptions = {'metalness': 'metallic', 'specularRoughness': 'roughness', 'emissionColor': 'emissive'} - for m in maps: - key = m if m not in naming_exceptions.keys() else naming_exceptions.get(m) - texture_attribute = m - for tex in file_connections.keys(): - if tex.find(texture_attribute) != -1: - logging.debug('Key, Value: {} {}.{}'.format(key, name, texture_attribute)) - material_properties[key] = {'useTexture': 'true', - 'textureMap': file_connections.get( - '{}.{}'.format(name, texture_attribute))} - else: - pass - - return material_properties - - -def get_blender_material_mapping(name, material_type, file_connections): - pass - - -def get_max_material_mapping(name, material_type, file_connections): - pass - diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/drag_and_drop.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/drag_and_drop.py deleted file mode 100755 index d02104b12c..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/drag_and_drop.py +++ /dev/null @@ -1,65 +0,0 @@ -# coding:utf-8 -#!/usr/bin/python -# -# 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 line is 75 characters ------------------------------------------- - -from PySide2 import QtWidgets, QtCore -from PySide2.QtCore import Signal - -class DragAndDrop(QtWidgets.QWidget): - drop_update = QtCore.Signal(list) - drop_over = QtCore.Signal(bool) - - def __init__(self, frame_color=None, highlight=None, parent=None): - super(DragAndDrop, self).__init__(parent) - - self.urls = [] - self.frame_color = frame_color - self.frame_highlight = highlight - self.setContentsMargins(0, 0, 0, 0) - self.setAcceptDrops(True) - - self.drag_and_drop_frame = QtWidgets.QFrame(self) - self.drag_and_drop_frame.setGeometry(0, 0, 5000, 5000) - self.drag_and_drop_frame.setStyleSheet('background-color:rgb({});'.format(self.frame_color)) - - def dragEnterEvent(self, e): - if e.mimeData().hasUrls: - e.accept() - self.drop_over.emit(True) - if self.frame_highlight: - self.drag_and_drop_frame.setStyleSheet('background-color:rgb({});'.format(self.frame_highlight)) - else: - e.ignore() - - def dragLeaveEvent(self, e): - self.drop_over.emit(False) - - if self.frame_highlight: - self.drag_and_drop_frame.setStyleSheet('background-color:rgb({});'.format(self.frame_color)) - - def dragMoveEvent(self, e): - if e.mimeData().hasUrls: - e.accept() - else: - e.ignore() - - def dropEvent(self, e): - if e.mimeData().hasUrls: - e.setDropAction(QtCore.Qt.CopyAction) - e.accept() - - for url in e.mimeData().urls(): - file_name = str(url.toLocalFile()) - self.urls.append(file_name) - self.drop_update.emit(self.urls) - if self.frame_highlight: - self.drag_and_drop_frame.setStyleSheet('background-color:rgb({});'.format(self.frame_color)) - else: - e.ignore() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/launcher.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/launcher.bat deleted file mode 100644 index d609c26897..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/launcher.bat +++ /dev/null @@ -1,85 +0,0 @@ -@echo off - -REM -REM Copyright (c) Contributors to the Open 3D Engine Project. -REM For complete copyright and license terms please see the LICENSE at the root of this distribution. -REM -REM SPDX-License-Identifier: Apache-2.0 OR MIT -REM -REM - -:: Set up and run LY Python CMD prompt -:: Sets up the DccScriptingInterface_Env, -:: Puts you in the CMD within the dev environment - -:: Set up window -TITLE Lumberyard DCC Scripting Interface Cmd -:: Use obvious color to prevent confusion (Grey with Yellow Text) -COLOR 8E - -%~d0 -cd %~dp0 - -:: Keep changes local -SETLOCAL enableDelayedExpansion - -:: This maps up to the \Dev folder -IF "%DEV_REL_PATH%"=="" (set DEV_REL_PATH=..\..\..\..\..\..\..) - -:: Change to root Lumberyard dev dir -:: Don't use the LY_DEV so we can test that ENVAR!!! -CD /d %DEV_REL_PATH% -set Rel_Dev=%CD% -echo Rel_Dev = %Rel_Dev% -:: Restore original directory -popd - -set DCCSI_PYTHON_INSTALL=%Rel_Dev%\Tools\Python\3.7.5\windows - -:: add to the PATH -SET PATH=%DCCSI_PYTHON_INSTALL%;%PATH% - -:: dcc scripting interface gem path -set DCCSIG_PATH=%Rel_Dev%\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface -echo DCCSIG_PATH = %DCCSIG_PATH% - -:: add to the PATH -SET PATH=%DCCSIG_PATH%;%PATH% - -:: Constant Vars (Global) -:: global debug (propogates) -IF "%DCCSI_GDEBUG%"=="" (set DCCSI_GDEBUG=false) -echo DCCSI_GDEBUG = %DCCSI_GDEBUG% -:: initiates debugger connection -IF "%DCCSI_DEV_MODE%"=="" (set DCCSI_DEV_MODE=false) -echo DCCSI_DEV_MODE = %DCCSI_DEV_MODE% -:: sets debugger, options: WING, PYCHARM -IF "%DCCSI_GDEBUGGER%"=="" (set DCCSI_GDEBUGGER=WING) -echo DCCSI_GDEBUGGER = %DCCSI_GDEBUGGER% - -echo. -echo _____________________________________________________________________ -echo. -echo ~ LY DCCsi, DCC Material Converter -echo _____________________________________________________________________ -echo. - -:: Change to root dir -CD /D %DCCSIG_PATH% - -:: add to the PATH -SET PATH=%DCCSIG_PATH%;%PATH% - -set PYTHONPATH=%DCCSIG_PATH%;%PYTHONPATH% - -CALL %DCCSI_PYTHON_INSTALL%\python.exe "%DCCSIG_PATH%\SDK\PythonTools\DCC_Material_Converter\standalone.py" - - -ENDLOCAL - -:: Return to starting directory -POPD - -:END_OF_FILE - -exit /b 0 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/main.py deleted file mode 100755 index f9359d7d82..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/main.py +++ /dev/null @@ -1,1026 +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 -# -# - -""" -Usage -===== -Put usage instructions here. - -Output -====== -Put output information here. - -Notes: -In order to run this, you'll need to verify that the "mayapy_path" class attribute corresponds to the location on -your machine. Currently I've just included mapping instructions for Maya StingrayPBS materials, although most of -the needed elements are in place to carry out additional materials inside of Maya pretty quickly moving forward. -I've marked areas that still need refinement (or to be added altogether) with TODO comments - -TODO- Docstrings need work... wanted to get descriptions in but they need to be set for Sphinx -TODO- Add 3ds Max interoperability -Links: -https://blender.stackexchange.com/questions/100497/use-blenders-bpy-in-projects-outside-blender -https://knowledge.autodesk.com/support/3ds-max/learn-explore/caas/CloudHelp/cloudhelp/2019/ENU/3DSMax-Batch/files/ -GUID-0968FF0A-5ADD-454D-B8F6-1983E76A4AF9-htm.html - -TODO- Look at dynaconf and wire in a solid means for configuration settings -TODO- This hasn't been "designed"- might be worth it to consider the visual design to ensure the most effective and - attractive UI -TODO- Allow revisions to Model - -Reading FBX file information (might come in handy later) --- Materials information can be extracted from ASCII fbx pretty easily, binary is possible but more difficult --- FBX files could be exported as ASCII files and I could use regex there to extract material information --- I couldn't get pyfbx_i42 to work, but purportedly it can extract information from binary files. You may just have -to use the specified python versions -""" -# built-ins -import collections -import logging -import subprocess -import json -import sys -import os -import re - -# should give access to Lumberyard Qt dlls and PySide2 -from PySide2 import QtWidgets, QtCore, QtGui -from PySide2.QtCore import Slot -from PySide2.QtWidgets import QApplication -import shiboken2 -from shiboken2 import wrapInstance - -# local imports -from model import MaterialsModel -from drag_and_drop import DragAndDrop -import dcc_material_mapping as dcc_map - -# global space -main_window_pointer = None -main_app_window = None - - -class MaterialsToLumberyard(QtWidgets.QWidget): - def __init__(self, output_material_type='PBR', cli_values=None, parent=None): - super(MaterialsToLumberyard, self).__init__(parent) - - self.app = QtWidgets.QApplication.instance() - self.setWindowFlags(QtCore.Qt.Window) - self.setGeometry(50, 50, 800, 520) - self.setObjectName('MaterialsToLumberyard') - self.setWindowTitle(' ') - self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowMinMaxButtonsHint) - self.isTopLevel() - - self.cli_enabled = cli_values - self.output_material_type = output_material_type - self.desktop_location = os.path.join(os.path.expanduser('~'), 'Desktop') - self.directory_path = os.path.dirname(os.path.abspath(__file__)) - self.mayapy_path = os.path.abspath("C:/Program Files/Autodesk/Maya2020/bin/mayapy.exe") - self.blender_path = self.get_blender_path() - self.bold_font_large = QtGui.QFont('Helvetica', 7, QtGui.QFont.Bold) - self.medium_font = QtGui.QFont('Helvetica', 7, QtGui.QFont.Normal) - self.blessed_file_extensions = 'ma mb fbx max blend'.split(' ') - - self.dcc_materials_dictionary = {} - self.lumberyard_materials_dictionary = {} - self.lumberyard_material_nodes = [] - self.target_file_list = [] - self.current_scene = None - self.model = None - self.total_materials = 0 - - self.main_container = QtWidgets.QVBoxLayout(self) - self.main_container.setContentsMargins(0, 0, 0, 0) - self.main_container.setAlignment(QtCore.Qt.AlignTop) - self.setLayout(self.main_container) - self.content_layout = QtWidgets.QVBoxLayout() - self.content_layout.setAlignment(QtCore.Qt.AlignTop) - self.content_layout.setContentsMargins(10, 3, 10, 5) - self.main_container.addLayout(self.content_layout) - - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # ---->> Header Bar - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - self.header_bar_layout = QtWidgets.QHBoxLayout() - self.lumberyard_logo_layout = QtWidgets.QHBoxLayout() - self.lumberyard_logo_layout.setAlignment(QtCore.Qt.AlignLeft) - logo_path = os.path.join(self.directory_path, 'resources', 'lumberyard_logo.png') - logo_pixmap = QtGui.QPixmap(logo_path) - self.lumberyard_logo = QtWidgets.QLabel() - self.lumberyard_logo.setPixmap(logo_pixmap) - self.lumberyard_logo_layout.addWidget(self.lumberyard_logo) - self.header_bar_layout.addLayout(self.lumberyard_logo_layout) - - self.switch_combobox_layout = QtWidgets.QHBoxLayout() - self.switch_combobox_layout.setAlignment(QtCore.Qt.AlignRight) - self.switch_layout_combobox = QtWidgets.QComboBox() - self.set_combobox_items_accessibility() - self.switch_layout_combobox.setFixedSize(250, 30) - self.combobox_items = ['Add Source Files', 'Source File List', 'DCC Material Values', 'Export Materials'] - self.switch_layout_combobox.setStyleSheet('QComboBox {padding-left:6px;}') - self.switch_layout_combobox.addItems(self.combobox_items) - self.switch_combobox_layout.addWidget(self.switch_layout_combobox) - self.header_bar_layout.addLayout(self.switch_combobox_layout) - - self.content_layout.addSpacing(5) - self.content_layout.addLayout(self.header_bar_layout) - - # ++++++++++++++++++++++++++++++++++++++++++++++++# - # File Source Table / Attributes (Stacked Layout) # - # ++++++++++++++++++++++++++++++++++++++++++++++++# - - self.content_stacked_layout = QtWidgets.QStackedLayout() - self.content_layout.addLayout(self.content_stacked_layout) - self.switch_layout_combobox.currentIndexChanged.connect(self.layout_combobox_changed) - - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # ---->> Add Source Files - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - frame_color_value = '75,75,75' - highlight_color_value = '20,106,30' - self.drag_and_drop_widget = DragAndDrop(frame_color_value, highlight_color_value) - self.drag_and_drop_widget.drop_update.connect(self.drag_and_drop_file_update) - self.drag_and_drop_widget.drop_over.connect(self.drag_and_drop_over) - self.drag_and_drop_layout = QtWidgets.QVBoxLayout() - self.drag_and_drop_layout.setContentsMargins(0, 0, 0, 0) - self.drag_and_drop_layout.setAlignment(QtCore.Qt.AlignCenter) - self.drag_and_drop_widget.setLayout(self.drag_and_drop_layout) - - start_message = 'Drag source files here, or use file browser button below to get started.' - self.drag_and_drop_label = QtWidgets.QLabel(start_message) - self.drag_and_drop_label.setStyleSheet('color: white;') - self.drag_and_drop_layout.addWidget(self.drag_and_drop_label) - self.drag_and_drop_layout.addSpacing(10) - - self.select_files_button_layout = QtWidgets.QHBoxLayout() - self.select_files_button_layout.setAlignment(QtCore.Qt.AlignCenter) - self.select_files_button = QtWidgets.QPushButton('Select Files') - self.select_files_button_layout.addWidget(self.select_files_button) - self.select_files_button.clicked.connect(self.select_files_button_clicked) - self.select_files_button.setFixedSize(80, 35) - self.drag_and_drop_layout.addLayout(self.select_files_button_layout) - self.content_stacked_layout.addWidget(self.drag_and_drop_widget) - - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # ---->> Files Table - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - self.target_files_table = QtWidgets.QTableWidget() - self.target_files_table.setFocusPolicy(QtCore.Qt.NoFocus) - self.target_files_table.setColumnCount(2) - self.target_files_table.setAlternatingRowColors(True) - self.target_files_table.setHorizontalHeaderLabels(['File List', '']) - self.target_files_table.horizontalHeader().setStyleSheet('QHeaderView::section ' - '{background-color: rgb(220, 220, 220); ' - 'padding-top:7px; padding-left:5px;}') - self.target_files_table.verticalHeader().hide() - files_header = self.target_files_table.horizontalHeader() - files_header.setFixedHeight(30) - files_header.setDefaultAlignment(QtCore.Qt.AlignLeft) - files_header.setContentsMargins(10, 10, 0, 0) - files_header.setDefaultSectionSize(60) - files_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) - files_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed) - self.target_files_table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) - self.content_stacked_layout.addWidget(self.target_files_table) - - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # ---->> Scene Information Table - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - self.material_tree_view = QtWidgets.QTreeView() - self.headers = ['Key', 'Value'] - self.material_tree_view.setStyleSheet('QTreeView::item {height:25px;} QHeaderView::section ' - '{background-color: rgb(220, 220, 220); height:30px; padding-left:10px}') - self.material_tree_view.setFocusPolicy(QtCore.Qt.NoFocus) - self.material_tree_view.setAlternatingRowColors(True) - self.material_tree_view.setUniformRowHeights(True) - self.content_stacked_layout.addWidget(self.material_tree_view) - - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # ---->> LY Material Definitions - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - self.lumberyard_material_definitions_widget = QtWidgets.QWidget() - self.lumberyard_material_definitions_layout = QtWidgets.QHBoxLayout(self.lumberyard_material_definitions_widget) - self.lumberyard_material_definitions_layout.setSpacing(0) - self.lumberyard_material_definitions_layout.setContentsMargins(0, 0, 0, 0) - self.lumberyard_material_definitions_frame = QtWidgets.QFrame(self.lumberyard_material_definitions_widget) - self.lumberyard_material_definitions_frame.setGeometry(0, 0, 5000, 5000) - self.lumberyard_material_definitions_frame.setStyleSheet('background-color:rgb(75,75,75);') - self.lumberyard_material_definitions_scroller = QtWidgets.QScrollArea() - self.scroller_widget = QtWidgets.QWidget() - self.scroller_layout = QtWidgets.QVBoxLayout() - self.scroller_widget.setLayout(self.scroller_layout) - self.lumberyard_material_definitions_scroller.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) - self.lumberyard_material_definitions_scroller.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.lumberyard_material_definitions_scroller.setWidgetResizable(True) - self.lumberyard_material_definitions_scroller.setWidget(self.scroller_widget) - self.lumberyard_material_definitions_layout.addWidget(self.lumberyard_material_definitions_scroller) - self.content_stacked_layout.addWidget(self.lumberyard_material_definitions_widget) - - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # ---->> File processing buttons - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - self.process_files_layout = QtWidgets.QHBoxLayout() - self.content_layout.addLayout(self.process_files_layout) - self.process_files_button = QtWidgets.QPushButton('Process Added Files') - self.process_files_button.setFixedHeight(50) - self.process_files_button.clicked.connect(self.process_listed_files_clicked) - self.process_files_layout.addWidget(self.process_files_button) - - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # ---->> Status bar / Loader - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - # TODO- Move all processing of files to another thread and display progress with loader - - self.status_bar = QtWidgets.QStatusBar() - self.status_bar.setStyleSheet('background-color: rgb(220, 220, 220);') - self.status_bar.setContentsMargins(0, 0, 0, 0) - self.status_bar.setSizeGripEnabled(False) - self.message_readout_label = QtWidgets.QLabel('Ready.') - self.message_readout_label.setStyleSheet('padding-left: 10px') - self.status_bar.addWidget(self.message_readout_label) - - self.progress_bar = QtWidgets.QProgressBar() - self.progress_bar_widget = QtWidgets.QWidget() - self.progress_bar_widget_layout = QtWidgets.QHBoxLayout() - self.progress_bar_widget_layout.setContentsMargins(0, 0, 0, 0) - self.progress_bar_widget_layout.setAlignment(QtCore.Qt.AlignRight) - self.progress_bar_widget.setLayout(self.progress_bar_widget_layout) - self.status_bar.addPermanentWidget(self.progress_bar_widget) - self.progress_bar_widget_layout.addWidget(self.progress_bar) - self.progress_bar.setFixedSize(180, 20) - self.main_container.addWidget(self.status_bar) - self.initialize() - - ############################ - # UI Display Layers ######## - ############################ - - def initialize(self): - if self.cli_enabled: - print('CLI ACCESS:::::::::::\nValues passed: {}'.format(self.cli_enabled)) - self.target_file_list = self.cli_enabled - self.process_file_list() - self.export_selected_materials() - - def populate_source_files_table(self): - """ - Adds selected files from the 'Source Files' section of the UI. This creates each item listing in the table - as well as adds a 'Remove' button that will clear corresponding item from the table. Processed files will - get color coded, based on whether or not the materials in the file could be successfully processed. Subsequent - searches will not clear items from the table currently, as each item acts as a register of materials that have - and have not yet been processed. - :return: - """ - self.target_files_table.setRowCount(0) - for index, entry in enumerate(self.target_file_list): - entry = entry[1] if type(entry) == list else entry - self.target_files_table.insertRow(index) - item = QtWidgets.QTableWidgetItem(' {}'.format(entry)) - self.target_files_table.setRowHeight(index, 45) - remove_button = QtWidgets.QPushButton('Remove') - remove_button.setFixedWidth(60) - remove_button.clicked.connect(self.remove_source_file_clicked) - self.target_files_table.setItem(index, 0, item) - self.target_files_table.setCellWidget(index, 1, remove_button) - - def populate_dcc_material_values_tree(self): - """ - Sets the materials model class to the file attribute tree. - :return: - """ - # TODO- Create mechanism for collapsing previously gathered materials, and or pushing them further down the list - self.material_tree_view.setModel(self.model) - self.material_tree_view.expandAll() - self.material_tree_view.resizeColumnToContents(0) - - def populate_export_materials_list(self): - """ - Once all materials have been analyzed inside of DCC applications, the 'Export Materials' view lists all - materials presented as their Lumberyard counterparts. Each listing displays a representation of the material - file based on its corresponding DCC material values and file connections. - :return: - """ - self.reset_export_materials_description() - for count, value in enumerate(self.lumberyard_materials_dictionary): - material_definition_node = MaterialNode([value, self.lumberyard_materials_dictionary[value]], count) - self.lumberyard_material_nodes.append(material_definition_node) - self.scroller_layout.addWidget(material_definition_node) - self.scroller_layout.addLayout(self.create_separator_line()) - - ############################ - # TBD ######## - ############################ - - def process_file_list(self): - """ - The entry point for reading DCC files and extracting values. Files are filtered and separated - by DCC app (based on file extensions) before processing is done. - - Supported DCC applications: - Maya (.ma, .mb, .fbx), 3dsMax(.max), Blender(.blend) - :return: - """ - files_dict = {'maya': [], 'max': [], 'blender': [], 'na': []} - for file_location in self.target_file_list: - file_name = os.path.basename(str(file_location)) - file_extension = os.path.splitext(file_name)[1] - target_application = self.get_target_application(file_extension) - if target_application in files_dict.keys(): - files_dict[target_application].append(file_location) - - for key, values in files_dict.items(): - try: - if key == 'maya' and len(values): - self.get_maya_material_values(values) - elif key == 'max' and len(values): - self.get_max_material_values(values) - elif key == 'blender' and len(values): - self.get_blender_material_values(values) - else: - pass - except Exception as e: - # TODO- Allow corrective actions or some display of errors if this fails? - logging.warning('Could not process files. Error: {}'.format(e)) - - if self.dcc_materials_dictionary: - self.set_transfer_status(self.dcc_materials_dictionary) - # Create Model with extracted values from file list - self.set_material_model() - # Setup Lumberyard Material File Values - self.set_export_materials_description() - # Update UI Layout - self.populate_export_materials_list() - self.switch_layout_combobox.setCurrentIndex(3) - self.set_ui_buttons() - self.message_readout_label.setText('Ready.') - - def reset_export_materials_description(self): - pass - - def reset_all_values(self): - pass - - def create_separator_line(self): - """ Convenience function for adding separation line to the UI. """ - layout = QtWidgets.QHBoxLayout() - line = QtWidgets.QLabel() - line.setFrameStyle(QtWidgets.QFrame.HLine | QtWidgets.QFrame.Sunken) - line.setLineWidth(1) - line.setFixedHeight(10) - layout.addWidget(line) - layout.setContentsMargins(8, 0, 8, 0) - return layout - - def export_selected_materials(self): - """ - This will eventually be revised to save material definitions in the proper place in the user's project folder, - but for now material definitions will be saved to the desktop. - :return: - """ - for node in self.lumberyard_material_nodes: - if node.material_name_checkbox.isChecked(): - output_path = os.path.dirname(node.material_info['sourceFile']) - node.material_info.pop('sourceFile') - output = os.path.join(output_path, '{}.material'.format(node.material_name)) - with open(output, 'w', encoding='utf-8') as material_file: - json.dump(node.material_info, material_file, ensure_ascii=False, indent=4) - - ############################ - # Getters/Setters ########## - ############################ - - @staticmethod - def get_target_application(file_extension): - """ - Searches compatible file extensions and returns one of three Application names- Maya, 3dsMax, or Blender. - :param file_extension: Passed file extension used to determine DCC Application it originated from. - :return: Returns the application corresponding to the extension if found- otherwise returns a Boolean None - """ - app_extensions = {'maya': ['.ma', '.mb', '.fbx'], 'max': ['.max'], 'blender': ['.blend']} - target_dcc_application = [key for key, values in app_extensions.items() if file_extension in values] - if target_dcc_application: - return target_dcc_application[0] - return None - - @staticmethod - def get_lumberyard_material_template(shader_type): - """ - Loads material descriptions from the Lumberyard installation, providing a template to compare and convert DCC - shaders to Lumberyard material definitions. This is the first step in the comparison. The second step is to - compare these values with specific mapping instructions for DCC Application and DCC material type to arrive at - a converted material. - :param shader_type: The type of Lumberyard shader to pair material attributes to (i.e. PBR Shader) - :return: File dictionary of the available boilerplate Lumberyard shader settings. - """ - definitions = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources', - '{}.template.material'.format(shader_type)) - if os.path.exists(definitions): - with open(definitions) as f: - return json.load(f) - - @staticmethod - def get_lumberyard_material_properties(name, dcc_app, material_type, file_connections): - """ - This system will probably need rethinking if DCCs and compatible materials grow. I've tried to keep this - flexible so that it can be expanded with more apps and materials. - - :param name: Material name from within the DCC application - :param dcc_app: The application that the material was sourced from - :param material_type: DCC material type - :param file_connections: Texture files found attached to the materials - """ - - material_properties = {} - if dcc_app == 'Maya': - material_properties = dcc_map.get_maya_material_mapping(name, material_type, file_connections) - elif dcc_app == 'Blender': - material_properties = dcc_map.get_blender_material_mapping(name, material_type, file_connections) - elif dcc_app == '3dsMax': - material_properties = dcc_map.get_max_material_mapping(name, material_type, file_connections) - else: - pass - return material_properties - - @staticmethod - def get_filename_increment(name): - """ - Convenience function that assists in ensuring that if any materials are encountered with the same name, an - underscore and number is appended to it to prevent overwrites. - :param name: The name of the material. The function searches the string for increment numbers, and either adds - one to any encountered, or adds an "_1" if passed name is the first duplicate encountered. - :return: The adjusted name with a unique incremental value. - """ - last_number = re.compile(r'(?:[^\d]*(\d+)[^\d]*)+') - number_found = last_number.search(name) - if number_found: - next_number = str(int(number_found.group(1)) + 1) - start, end = number_found.span(1) - name = name[:max(end - len(next_number), start)] + next_number + name[end:] - return name - - def get_maya_material_values(self, target_files): - """ - Launches Maya Standalone and processes list of materials for each scene passed to the 'target_files' argument. - Also sets the environment paths needed for an instance of Maya's Python distribution. After files are processed - a single dictionary of scene materials is returned, and added to the "materials_dictionary" scene attribute. - :param target_files: List of files filtered from total list of files requested for processing that have a - Maya file extension - :return: - """ - - # TODO- Set load process to a separate thread and wire load progress bar up - - try: - script_path = str(os.path.join(self.directory_path, 'maya_materials.py')) - target_files.append(self.total_materials) - runtime_env = os.environ.copy() - runtime_env['MAYA_LOCATION'] = os.path.dirname(self.mayapy_path) - runtime_env['PYTHONPATH'] = os.path.dirname(self.mayapy_path) - command = f'{self.mayapy_path} "{script_path}"' - for file in target_files: - command += f' "{file}"' - p = subprocess.Popen(command, shell=False, env=runtime_env, stdout=subprocess.PIPE) - output = p.communicate()[0] - self.set_material_dictionary(json.loads(output)) - except Exception as e: - logging.warning('maya error: {}'.format(e)) - - def get_max_material_values(self, target_files): - """ - This has not been implemented yet. - - :param target_files: List of files filtered from total list of files requested for processing that have a - .max file extension - :return: - """ - logging.debug('Max Target file: {}'.format(target_files)) - - def get_blender_material_values(self, target_files): - """ - This has not been implemented yet. - - :param target_files: List of files filtered from total list of files requested for processing that have a - .blend file extension - :return: - """ - logging.debug('Blender Target file: {}'.format(target_files)) - script_path = str(os.path.join(self.directory_path, 'blender_materials.py')) - target_files.append(self.total_materials) - p = subprocess.Popen([self.blender_path, '--background', '--python', script_path, '--', target_files]) - output = p.communicate()[0] - self.set_material_dictionary(json.loads(output)) - - def get_blender_path(self): - """ - Finds latest Blender version installed on the user machine for command line file processing. - - :return: Most current version available (or none) - """ - blender_base_directory = os.path.join(os.path.join('C:\\', 'Program Files', 'Blender Foundation')) - blender_versions_found = [] - for (directory_path, directory_name, filenames) in os.walk(blender_base_directory): - for filename in filenames: - if filename == 'blender.exe': - blender_versions_found.append(os.path.join(directory_path, filename)) - - if blender_versions_found: - return max(blender_versions_found, key=os.path.getctime) - else: - return None - - def set_combobox_items_accessibility(self): - """ - Locks items from within the combobox until the sections they connect to have content - :return: - """ - # TODO- Add this functionality - pass - - def set_transfer_status(self, transfer_info): - """ - Colorizes listings in the 'Source Files' view of the UI after processing to green or red, indicating whether or - not scene analysis successfully returned compatible materials and their values. - :param transfer_info: Each file the scripts attempt to process return a receipt of the success or failure of - the analysis. - :return: - """ - - # TODO- Include some way to get error information if analysis fails, and potentially offer the means to - # repair values as they map to intended Lumberyard shader type - - for row in range(self.target_files_table.rowCount()): - for key, values in transfer_info.items(): - row_path = self.target_files_table.item(row, 0).text().strip() - scene_processed = {x for x in transfer_info if values['SceneName'].replace('\\', '/') == row_path} - if scene_processed: - self.target_files_table.item(row, 0).setBackground(QtGui.QColor(192, 255, 171)) - break - else: - self.target_files_table.item(row, 0).setBackground(QtGui.QColor(255, 177, 171)) - - def set_export_materials_description(self): - root = self.model.rootItem - for row in range(self.model.rowCount()): - source_file = self.model.get_attribute_value('SceneName', root.child(row)) - name = self.model.get_attribute_value('MaterialName', root.child(row)) - material_type = self.model.get_attribute_value('MaterialType', root.child(row)) - dcc_app = self.model.get_attribute_value('DccApplication', root.child(row)) - file_connections = {} - shader_attributes = {} - - for childIndex in range(root.child(row).childCount()): - child_item = root.child(row).child(childIndex) - child_value = child_item.itemData - if child_item.childCount(): - target_dict = file_connections if child_value[0] == 'FileConnections' else shader_attributes - for subChildIndex in range(child_item.childCount()): - sub_child_data = child_item.child(subChildIndex).itemData - target_dict[sub_child_data[0]] = sub_child_data[1] - self.set_material_description(source_file, name, dcc_app, material_type, file_connections) - - def set_material_dictionary(self, dcc_dictionary): - """ - Adds all material descriptions pulled from each DCC file analyzed to the "materials_dictionary" class attribute. - This function runs each time a subprocess is launched to gather DCC application material values. - :param dcc_dictionary: The dictionary of values for each material analyzed by each specific DCC file list - return analyzed values - :return: - """ - logging.debug('DCC Dictionary: {}'.format(json.dumps(dcc_dictionary, indent=4))) - self.total_materials += len(dcc_dictionary) - self.dcc_materials_dictionary.update(dcc_dictionary) - - def set_material_model(self, initialize=True): - """ - Once all materials have been gathered across a selected file set query, this organizes the values into a - QT Model Class - :param initialize: Default is set to boolean True. If a model has already been established in the current - session, the initialize parameter would be set to false, and the values added to the Model. All changes to - the model would then be redistributed to other informational views in the UI. - :return: - """ - if initialize: - self.model = MaterialsModel(self.headers, self.dcc_materials_dictionary) - else: - self.model.update() - self.dcc_materials_dictionary.clear() - self.populate_dcc_material_values_tree() - - def set_ui_buttons(self): - """ - Handles UI buttons for each of the three stacked layout views (Source Files, DCC Material Values, - Export Materials) - :return: - """ - display_index = self.content_stacked_layout.currentIndex() - self.switch_layout_combobox.setEnabled(True) - self.process_files_button.setText('Process Listed Files') - # Add Source Files Layout ------------------------------->> - if display_index == 0: - self.process_files_button.setEnabled(True) - - # Source File List -------------------------------------->> - elif display_index == 1: - self.process_files_button.setEnabled(True) - - # DCC Material Values Layout ---------------------------->> - elif display_index == 2: - self.process_files_button.setEnabled(False) - - # Export Materials Layout ------------------------------->> - else: - self.process_files_button.setText('Export Selected Materials') - if self.lumberyard_materials_dictionary: - self.process_files_button.setEnabled(True) - - def set_material_description(self, source_file, name, dcc_app, material_type, file_connections): - """ - Build dictionary for material description based on extracted values - - :param source_file: The file that the material was extracted from - :param name: Name of material - :param dcc_app: Source file type of material (Maya, Blender or 3ds Max) - :param material_type: Material type within app (i.e. Stingray PBS) - :param file_connections: Texture files found connected to the shader - :return: - """ - - default_settings = self.get_lumberyard_material_template('standardPBR') - material = collections.OrderedDict(sourceFile=source_file, description=name, - materialType=default_settings.get('materialType'), - parentMaterial=default_settings.get('parentMaterial'), - propertyLayoutVersion=default_settings.get('propertyLayoutVersion'), - properties=self.get_lumberyard_material_properties(name, dcc_app, - material_type, - file_connections)) - name += self.output_material_type - self.lumberyard_materials_dictionary[name if name not in self.lumberyard_materials_dictionary.keys() else - self.get_filename_increment(name)] = material - - ############################ - # Button Actions ########### - ############################ - - def remove_source_file_clicked(self): - """ - In the Source File view of the UI layout, this will remove the listed file in its respective row. If files - have not been processed yet, it prevents that file from being analyzed. If the files have already been - analyzed, this will remove the materials from stored values. - :return: - """ - file_index = self.target_files_table.indexAt(self.sender().pos()) - del self.target_file_list[file_index.row()] - self.populate_files_table() - - def process_listed_files_clicked(self): - """ - The button serves a dual purpose, depending on the current layout of the window. 'Process listed files' - initiates the DCC file analysis that extracts material information. In the "Export Materials" layout, this - button (for now) will export material files corresponding to each analyzed material. Exported material files - are routed to the directories of the respective files processed. - :return: - """ - - if self.sender().text() == 'Process Added Files': - self.message_readout_label.setText('Gathering Material Information...') - self.app.processEvents() - self.process_file_list() - else: - self.export_selected_materials() - - def select_files_button_clicked(self): - """ - This dialog allows user to select DCC files to be processed for the materials present for conversion. - :return: - """ - - # TODO- Eventually it might be worth it to allow files from multiple locations to be selected. Currently - # this only allows single/multiple files from a single directory to be selected, although drag and drop - # allows multiple locations - - dialog = QtWidgets.QFileDialog(self, 'Shift-Select Target Files', self.desktop_location) - dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) - dialog.setNameFilter('Compatible Files (*.ma *.mb *.fbx *.max *.blend)') - dialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True) - file_view = dialog.findChild(QtWidgets.QListView, 'listView') - - # Workaround for selecting multiple files with File Dialog - if file_view: - file_view.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) - f_tree_view = dialog.findChild(QtWidgets.QTreeView) - if f_tree_view: - f_tree_view.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) - - if dialog.exec_() == QtWidgets.QDialog.Accepted: - self.target_file_list += dialog.selectedFiles() - if self.target_file_list: - self.populate_source_files_table() - self.message_readout_label.setText('Source files added: {}'.format(len(self.target_file_list))) - self.process_files_button.setEnabled(True) - - def layout_combobox_changed(self): - """ - Handles main window layout combobox index change. - :return: - """ - self.content_stacked_layout.setCurrentIndex(self.switch_layout_combobox.currentIndex()) - self.set_ui_buttons() - - def reset_clicked(self): - """ - Brings the application and all variables back to their initial state. - :return: - """ - self.reset_all_values() - - ############################ - # Slots #################### - ############################ - - @Slot(list) - def drag_and_drop_file_update(self, file_list): - for file in file_list: - if os.path.basename(file).split('.')[-1] in self.blessed_file_extensions: - self.target_file_list.append(file) - self.drag_and_drop_widget.urls.clear() - self.populate_source_files_table() - self.message_readout_label.setText('Source files added: {}'.format(len(self.target_file_list))) - self.drag_and_drop_label.setStyleSheet('color: white;') - - @Slot(bool) - def drag_and_drop_over(self, is_over): - if is_over: - self.drag_and_drop_label.setStyleSheet('color: rgb(0, 255, 0);') - else: - self.drag_and_drop_label.setStyleSheet('color: white;') - - -class MaterialNode(QtWidgets.QWidget): - def __init__(self, material_info, current_position, parent=None): - super(MaterialNode, self).__init__(parent) - - self.material_name = material_info[0] - self.material_info = material_info[1] - self.current_position = current_position - self.property_settings = {} - - self.small_font = QtGui.QFont("Helvetica", 7, QtGui.QFont.Bold) - self.bold_font = QtGui.QFont("Helvetica", 8, QtGui.QFont.Bold) - self.main_layout = QtWidgets.QVBoxLayout() - self.main_layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.main_layout) - - self.background_frame = QtWidgets.QFrame(self) - self.background_frame.setGeometry(0, 0, 5000, 5000) - self.background_frame.setStyleSheet('background-color:rgb(220, 220, 220);') - - # ######################## - # Title Bar - # ######################## - - self.title_bar_widget = QtWidgets.QWidget() - self.title_bar_layout = QtWidgets.QHBoxLayout(self.title_bar_widget) - self.title_bar_layout.setContentsMargins(10, 0, 10, 0) - self.title_bar_layout.setAlignment(QtCore.Qt.AlignTop) - self.title_bar_frame = QtWidgets.QFrame(self.title_bar_widget) - self.title_bar_frame.setGeometry(0, 0, 5000, 40) - self.title_bar_frame.setStyleSheet('background-color:rgb(193,154,255);') - self.main_layout.addWidget(self.title_bar_widget) - self.material_name_checkbox = QtWidgets.QCheckBox(self.material_name) - self.material_name_checkbox.setFixedHeight(35) - self.material_name_checkbox.setStyleSheet('spacing:10px; color:white') - self.material_name_checkbox.setFont(self.bold_font) - self.material_name_checkbox.setChecked(True) - self.title_bar_layout.addWidget(self.material_name_checkbox) - - self.material_file_layout = QtWidgets.QHBoxLayout() - self.material_file_layout.setAlignment(QtCore.Qt.AlignRight) - self.source_file = QtWidgets.QLabel(os.path.basename(self.material_info['sourceFile'])) - self.source_file.setStyleSheet('color:white;') - self.source_file.setFont(self.small_font) - self.material_file_layout.addWidget(self.source_file) - self.material_file_layout.addSpacing(10) - - self.edit_button = QtWidgets.QPushButton('Edit') - self.edit_button.clicked.connect(self.edit_button_clicked) - self.edit_button.setFixedWidth(55) - self.material_file_layout.addWidget(self.edit_button) - self.title_bar_layout.addLayout(self.material_file_layout) - - self.information_layout = QtWidgets.QHBoxLayout() - self.information_layout.setContentsMargins(10, 0, 10, 10) - self.main_layout.addLayout(self.information_layout) - - # ######################## - # Details layout - # ######################## - - self.details_layout = QtWidgets.QVBoxLayout() - self.details_layout.setAlignment(QtCore.Qt.AlignTop) - self.details_groupbox = QtWidgets.QGroupBox("Details") - self.details_groupbox.setFixedWidth(200) - self.details_groupbox.setStyleSheet("QGroupBox {font:bold; border: 1px solid silver; " - "margin-top: 6px;} QGroupBox::title { color: rgb(150, 150, 150); " - "subcontrol-position: top left;}") - self.details_layout.addSpacing(15) - self.material_type_label = QtWidgets.QLabel('Material Type') - self.material_type_label.setStyleSheet('padding-left: 6px; color: white; background-color:rgb(175, 175, 175);') - self.material_type_label.setFixedHeight(25) - self.material_type_label.setFont(self.bold_font) - self.details_layout.addWidget(self.material_type_label) - - self.material_type_combobox = QtWidgets.QComboBox() - self.material_type_combobox.setFixedHeight(30) - self.material_type_combobox.setStyleSheet('QCombobox QAbstractItemView { padding-left: 15px; }') - material_type_items = [' Standard PBR'] - self.material_type_combobox.addItems(material_type_items) - self.details_layout.addWidget(self.material_type_combobox) - self.details_layout.addSpacing(10) - - self.description_label = QtWidgets.QLabel('Description') - self.description_label.setStyleSheet('padding-left: 6px; color: white; background-color:rgb(175, 175, 175);') - self.description_label.setFixedHeight(25) - self.description_label.setFont(self.bold_font) - self.details_layout.addWidget(self.description_label) - - self.description_box = QtWidgets.QTextEdit('This space is reserved for additional information.') - self.details_layout.addWidget(self.description_box) - self.information_layout.addWidget(self.details_groupbox) - self.details_groupbox.setLayout(self.details_layout) - - # ######################## - # Properties layout - # ######################## - - self.properties_layout = QtWidgets.QVBoxLayout() - self.properties_layout.setAlignment(QtCore.Qt.AlignTop) - self.properties_groupbox = QtWidgets.QGroupBox("Properties") - self.properties_groupbox.setFixedWidth(150) - self.properties_groupbox.setStyleSheet("QGroupBox {font:bold; border: 1px solid silver; " - "margin-top: 6px;} QGroupBox::title { color: rgb(150, 150, 150); " - "subcontrol-position: top left;}") - self.properties_list_widget = QtWidgets.QListWidget() - self.material_properties = ['ambientOcclusion', 'baseColor', 'emissive', 'metallic', 'roughness', 'specularF0', - 'normal', 'opacity'] - self.properties_list_widget.addItems(self.material_properties) - self.properties_list_widget.itemSelectionChanged.connect(self.property_selection_changed) - self.properties_layout.addSpacing(15) - self.properties_layout.addWidget(self.properties_list_widget) - self.information_layout.addWidget(self.properties_groupbox) - self.properties_groupbox.setLayout(self.properties_layout) - - # ######################## - # Attributes layout - # ######################## - - self.attributes_layout = QtWidgets.QVBoxLayout() - self.attributes_layout.setAlignment(QtCore.Qt.AlignTop) - self.attributes_groupbox = QtWidgets.QGroupBox("Attributes") - self.attributes_groupbox.setStyleSheet("QGroupBox {font:bold; border: 1px solid silver; " - "margin-top: 6px;} QGroupBox::title { color: rgb(150, 150, 150); " - "subcontrol-position: top left;}") - self.information_layout.addWidget(self.attributes_groupbox) - self.attributes_layout.addSpacing(15) - self.attributes_table = QtWidgets.QTableWidget() - self.attributes_table.setFocusPolicy(QtCore.Qt.NoFocus) - self.attributes_table.setColumnCount(2) - self.attributes_table.setAlternatingRowColors(True) - self.attributes_table.setHorizontalHeaderLabels(['Attribute', 'Value']) - self.attributes_table.verticalHeader().hide() - attributes_table_header = self.attributes_table.horizontalHeader() - attributes_table_header.setStyleSheet('QHeaderView::section {background-color: rgb(220, 220, 220);}') - attributes_table_header.setDefaultAlignment(QtCore.Qt.AlignLeft) - attributes_table_header.setContentsMargins(10, 10, 0, 0) - attributes_table_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) - attributes_table_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) - attributes_table_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Interactive) - self.attributes_layout.addWidget(self.attributes_table) - self.attributes_groupbox.setLayout(self.attributes_layout) - self.initialize_display_values() - - def initialize_display_values(self): - """ - Initializes all of the widget item information for material based on the DCC application info the class has - been passed. - :return: - """ - for material_property in self.material_properties: - if material_property in self.material_info.get('properties'): - self.property_settings[material_property] = self.material_info['properties'].get(material_property) - current_row = self.material_properties.index(material_property) - current_item = self.properties_list_widget.takeItem(current_row) - self.properties_list_widget.insertItem(0, current_item) - else: - self.property_settings[material_property] = 'inactive' - current_row = self.material_properties.index(material_property) - item = self.properties_list_widget.item(current_row) - item.setFlags(item.flags() & ~QtCore.Qt.ItemIsEnabled) - item.setFlags(item.flags() & ~QtCore.Qt.ItemIsSelectable) - - self.properties_list_widget.setCurrentRow(0) - self.set_attributes_table(self.get_selected_property()) - - def set_attributes_table(self, selected_property): - """ - Displays the key, value pairs for the item selected in the Properties list widget - :param selected_property: The item in the Properties list widget that is currently selected. Only active - values are displayed. - :return: - """ - self.attributes_table.setRowCount(0) - row_count = 0 - for key, value in self.property_settings[selected_property].items(): - self.attributes_table.insertRow(row_count) - key_item = QtWidgets.QTableWidgetItem(key) - self.attributes_table.setItem(row_count, 0, key_item) - value_item = QtWidgets.QTableWidgetItem(value) - self.attributes_table.setItem(row_count, 1, value_item) - row_count += 1 - - def get_selected_property(self): - """ - Convenience function to get current value selected in the Properties list widget. - :return: - """ - return self.properties_list_widget.currentItem().text() - - def update_model(self): - """ - Not sure if this will go away, but if desired, I could make attribute values able to be revised after - materials have been scraped from the DCC materials - :return: - """ - pass - - def edit_button_clicked(self): - """ - This is in place in the event that we want to allow material revisions for properties to be made after - DCC processing step has already been executed. The idea would basically be to surface an editable - table where values can be added, removed or changed within the final material definition. - :return: - """ - logging.debug('Edit button clicked') - - def property_selection_changed(self): - """ - Fired when index of list view selected property selection has changed. - :return: - """ - self.set_attributes_table(self.get_selected_property()) - - -def is_valid_file(file_name): - """ - The acts as a clearinghouse for DCC file types supported by the script - :param file_name: Reads the extension of the filename for filtering - :return: - """ - target_extensions = 'ma mb fbx blend max'.split(' ') - if file_name.split('.')[-1] in target_extensions: - return True - return False - - -def launch_material_converter(window_type='standalone', material_type='PBR', target_files=None): - """ - The setup for this will be revised once this is fully integrated into the DCCsi system. Currently only the - standalone (default) and command line entry points work as intended. - :param window_type: The method of access for material conversion (standalone, command_line, maya_native, max_native) - :param material_type: Type of output material desired for import into Lumberyard. Currently only PBR is supported - :param target_files: DCC app files to process for converted Lumberyard materials - :return: - """ - if window_type == 'command_line': - MaterialsToLumberyard(material_type, target_files) - elif window_type == 'maya_native': - from maya import OpenMayaUI as omui - main_window_pointer = omui.MQtUtil.mainWindow() - main_app_window = wrapInstance(long(main_window_pointer), QtWidgets.QWidget) - MaterialsToLumberyard(material_type, None, main_app_window) - elif window_type == 'max_native': - from pymxs import runtime as rt - main_window_pointer = QtWidgets.QWidget.find(rt.windows.getMAXHWND()) - main_app_window = shiboken2.wrapInstance(shiboken2.getCppPointer(main_window_pointer)[0], QtWidgets.QMainWindow) - MaterialsToLumberyard(material_type, None, main_app_window) - else: - app = QApplication(sys.argv) - app_ui = MaterialsToLumberyard() - app_ui.show() - sys.exit(app.exec_()) - - -if __name__ == '__main__': - launch_material_converter() - diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/maya_materials.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/maya_materials.py deleted file mode 100755 index 4e66f83661..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/maya_materials.py +++ /dev/null @@ -1,169 +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 -# - -from PySide2 import QtCore -import maya.standalone -maya.standalone.initialize(name='python') -import maya.cmds as mc -import collections -import logging -import json -import sys -import os - - -for handler in logging.root.handlers[:]: - logging.root.removeHandler(handler) - -logging.basicConfig(level=logging.INFO, - format='%(name)s - %(levelname)s - %(message)s', - datefmt='%m-%d %H:%M', - filename='output.log', - filemode='w') - - -class MayaMaterials(QtCore.QObject): - def __init__(self, files_list, materials_count, parent=None): - super(MayaMaterials, self).__init__(parent) - - self.files_list = files_list - self.current_scene = None - self.materials_dictionary = {} - self.materials_count = int(materials_count) - self.get_material_information() - - def get_material_information(self): - """ - Main entry point for the material information extraction. Because this class is run - in Standalone mode as a subprocess, the list is passed as a string- some parsing/measures - need to be taken in order to separate values that originated as a list before passed. - - :return: A dictionary of all of the materials gathered. Sent back to main UI through stdout - """ - for target_file in file_list: - self.current_scene = os.path.abspath(target_file.replace('\'', '')) - mc.file(self.current_scene, open=True, force=True) - self.set_material_descriptions() - json.dump(self.materials_dictionary, sys.stdout) - - @staticmethod - def get_materials(target_mesh): - """ - Gathers a list of all materials attached to each mesh's shader - - :param target_mesh: The target mesh to pull attached material information from. - :return: List of unique material values attached to the mesh passed as an argument. - """ - shading_group = mc.listConnections(target_mesh, type='shadingEngine') - materials = mc.ls(mc.listConnections(shading_group), materials=1) - return list(set(materials)) - - @staticmethod - def get_shader(material_name): - """ - Convenience function for obtaining the shader that the specified material (as an argument) - is attached to. - - :param material_name: Takes the material name as an argument to get associated shader object - :return: - """ - connections = mc.listConnections(material_name, type='shadingEngine')[0] - shader_name = '{}.surfaceShader'.format(connections) - shader = mc.listConnections(shader_name)[0] - return shader - - def get_shader_information(self, shader, material_mesh): - """ - Helper function for extracting shader/material attributes used to form the DCC specific dictionary - of found material values for conversion. - - :param shader: The target shader object to analyze - :param material_mesh: The material mesh needs to be passed to search for textures attached to it. - :return: Complete set (in the form of two dictionaries) of file connections and material attribute values - """ - shader_file_connections = {} - materials = self.get_materials(material_mesh) - for material in materials: - material_files = [x for x in mc.listConnections(material, plugs=1, source=1) if x.startswith('file')] - for file_name in material_files: - file_texture = mc.getAttr('{}.fileTextureName'.format(file_name.split('.')[0])) - if os.path.basename(file_texture).split('.')[-1] != 'dds': - key_name = mc.listConnections(file_name, plugs=1, source=1)[0] - shader_file_connections[key_name] = file_texture - - shader_attributes = {} - for shader_attribute in mc.listAttr(shader, s=True, iu=True): - try: - shader_attributes[str(shader_attribute)] = str(mc.getAttr('{}.{}'.format(shader, shader_attribute))) - except Exception as e: - logging.error('MayaAttributeError: {}'.format(e)) - - return shader_file_connections, shader_attributes - - def set_material_dictionary(self, material_name, material_type, material_mesh): - """ - When a unique material has been found, this creates a dictionary entry with all relevant material values. This - includes material attributes as well as attached file textures. Later in the process this information is - leveraged when creating the Lumberyard material definition. - - :param material_name: The name attached to the material - :param material_type: Specific type of material (Arnold, Stingray, etc.) - :param material_mesh: Mesh that the material is applied to - :return: - """ - self.materials_count += 1 - shader = self.get_shader(material_name) - shader_file_connections, shader_attributes = self.get_shader_information(shader, material_mesh) - material_dictionary = collections.OrderedDict(MaterialName=material_name, MaterialType=material_type, - DccApplication='Maya', AppliedMesh=material_mesh, - FileConnections=shader_file_connections, - SceneName=str(self.current_scene), - MaterialAttributes=shader_attributes) - material_name = 'Material_{}'.format(self.materials_count) - self.materials_dictionary[material_name] = material_dictionary - logging.info('\n\n:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\n' - 'MATERIAL DEFINITION: {} \n' - ':::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\n{}'.format( - self.materials_dictionary[material_name]['MaterialType'], - json.dumps(self.materials_dictionary[material_name], indent=4))) - - def set_material_descriptions(self): - """ - This function serves as the clearinghouse for all analyzed materials passing through the system. - It will determine whether or not the found material has already been processed, or if it needs to - be added to the final material dictionary. In the event that an encountered material has already - been processed, this function creates a register of all meshes it is applied to in the 'AppliedMesh' - attribute. - :return: - """ - scene_geo = mc.ls(v=True, geometry=True) - for target_mesh in scene_geo: - material_list = self.get_materials(target_mesh) - for material_name in material_list: - material_type = mc.nodeType(material_name) - - if material_type != 'lambert': - material_listed = [x for x in self.materials_dictionary - if self.materials_dictionary[x]['MaterialName'] == material_name] - - if not material_listed: - self.set_material_dictionary(str(material_name), str(material_type), str(target_mesh)) - else: - mesh_list = self.materials_dictionary[material_name].get('AppliedMesh') - if not isinstance(mesh_list, list): - self.materials_dictionary[str(material_name)]['AppliedMesh'] = [mesh_list, target_mesh] - else: - mesh_list.append(target_mesh) - - -# ++++++++++++++++++++++++++++++++++++++++++++++++# -# Maya Specific Shader Mapping # -# ++++++++++++++++++++++++++++++++++++++++++++++++# - -file_list = sys.argv[1:-1] -count = sys.argv[-1] -instance = MayaMaterials(file_list, count) - diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/model.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/model.py deleted file mode 100755 index 3c23635438..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/model.py +++ /dev/null @@ -1,154 +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 -# -# - -from PySide2.QtCore import QAbstractItemModel, QModelIndex, Qt - - -class MaterialsModel(QAbstractItemModel): - def __init__(self, headers, data, parent=None): - super(MaterialsModel, self).__init__(parent) - - self.rootItem = TreeNode(headers) - self.parents = [self.rootItem] - self.indentations = [0] - self.create_data(data) - - def create_data(self, data, indent=-1): - """ - Recursive loop that structures Model data into tree form. - :param data: Row information. - :param indent: Column information. This helps to facilitate the creation of nested rows. - :return: - """ - if type(data) == dict: - indent += 1 - position = 4 * indent - for key, value in data.items(): - if position > self.indentations[-1]: - if self.parents[-1].childCount() > 0: - self.parents.append(self.parents[-1].child(self.parents[-1].childCount() - 1)) - self.indentations.append(position) - else: - while position < self.indentations[-1] and len(self.parents) > 0: - self.parents.pop() - self.indentations.pop() - parent = self.parents[-1] - parent.insertChildren(parent.childCount(), 1, parent.columnCount()) - parent.child(parent.childCount() - 1).setData(0, key) - value_string = str(value) if type(value) != dict else str('') - parent.child(parent.childCount() - 1).setData(1, value_string) - try: - self.create_data(value, indent) - except RuntimeError: - pass - - @staticmethod - def get_attribute_value(search_string, search_column): - """ Convenience function for quickly accessing row information based on attribute keys. """ - for childIndex in range(search_column.childCount()): - child_item = search_column.child(childIndex) - child_value = child_item.itemData - if child_value[0] == search_string: - return child_value[1] - return None - - def index(self, row, column, index=QModelIndex()): - """ Returns the index of the item in the model specified by the given row, column and parent index """ - if not self.hasIndex(row, column, index): - return QModelIndex() - if not index.isValid(): - item = self.rootItem - else: - item = index.internalPointer() - - child = item.child(row) - if child: - return self.createIndex(row, column, child) - return QModelIndex() - - def parent(self, index): - """ - Returns the parent of the model item with the given index If the item has no parent, - an invalid QModelIndex is returned - """ - if not index.isValid(): - return QModelIndex() - item = index.internalPointer() - if not item: - return QModelIndex() - - parent = item.parentItem - if parent == self.rootItem: - return QModelIndex() - else: - return self.createIndex(parent.childNumber(), 0, parent) - - def rowCount(self, index=QModelIndex()): - """ - Returns the number of rows under the given parent. When the parent is valid it means that - rowCount is returning the number of children of parent - """ - if index.isValid(): - parent = index.internalPointer() - else: - parent = self.rootItem - return parent.childCount() - - def columnCount(self, index=QModelIndex()): - """ Returns the number of columns for the children of the given parent """ - return self.rootItem.columnCount() - - def data(self, index, role=Qt.DisplayRole): - """ Returns the data stored under the given role for the item referred to by the index """ - if index.isValid() and role == Qt.DisplayRole: - return index.internalPointer().data(index.column()) - elif not index.isValid(): - return self.rootItem.data(index.column()) - - def headerData(self, section, orientation, role=Qt.DisplayRole): - """ Returns the data for the given role and section in the header with the specified orientation """ - if orientation == Qt.Horizontal and role == Qt.DisplayRole: - return self.rootItem.data(section) - - -class TreeNode(object): - def __init__(self, data, parent=None): - self.parentItem = parent - self.itemData = data - self.children = [] - - def child(self, row): - return self.children[row] - - def childCount(self): - return len(self.children) - - def childNumber(self): - if self.parentItem is not None: - return self.parentItem.children.index(self) - - def columnCount(self): - return len(self.itemData) - - def data(self, column): - return self.itemData[column] - - def insertChildren(self, position, count, columns): - if position < 0 or position > len(self.children): - return False - for row in range(count): - data = [v for v in range(columns)] - item = TreeNode(data, self) - self.children.insert(position, item) - - def parent(self): - return self.parentItem - - def setData(self, column, value): - if column < 0 or column >= len(self.itemData): - return False - self.itemData[column] = value diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/standalone.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/standalone.py deleted file mode 100644 index e7a8445953..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/standalone.py +++ /dev/null @@ -1,37 +0,0 @@ -# coding:utf-8 -#!/usr/bin/python -# -# 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 -# - -"""Boostraps and Starts Standalone DCC Material Converter utility""" - -# built in's -import os -import site - -# ------------------------------------------------------------------------- -# \dev\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\SDK\PythonTools\DCC_Material_Converter\standalone.py -_MODULE_PATH = os.path.abspath(__file__) - -_DCCSIG_REL_PATH = "../../../.." -_DCCSIG_PATH = os.path.join(_MODULE_PATH, _DCCSIG_REL_PATH) -_DCCSIG_PATH = os.path.normpath(_DCCSIG_PATH) - -_DCCSIG_PATH = os.getenv('DCCSIG_PATH', - os.path.abspath(_DCCSIG_PATH)) - -# we don't have access yet to the DCCsi Lib\site-packages -site.addsitedir(_DCCSIG_PATH) # PYTHONPATH - -# azpy bootstrapping and extensions -import azpy.config_utils -_config = azpy.config_utils.get_dccsi_config() -settings = _config.get_config_settings(setup_ly_pyside=True) - -from main import launch_material_converter - -launch_material_converter() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/Launcher/main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/Launcher/main.py deleted file mode 100644 index 5812755035..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/Launcher/main.py +++ /dev/null @@ -1,74 +0,0 @@ -# coding:utf-8 -#!/usr/bin/python -# coding:utf-8 -#!/usr/bin/python -# -# 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 line is 75 characters ------------------------------------------- -# built-ins -import os -import sys -import logging as _logging - -# azpy extensions -import azpy.config_utils -_config = azpy.config_utils.get_dccsi_config() -settings = _config.get_config_settings(setup_ly_pyside=True) - -# 3rd Party (we may or do provide) -from pathlib import Path -from pathlib import PurePath - -# Lumberyard extensions -from azpy.env_bool import env_bool -from azpy.constants import ENVAR_DCCSI_GDEBUG -from azpy.constants import ENVAR_DCCSI_DEV_MODE - -# ------------------------------------------------------------------------- -# set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, settings.DCCSI_GDEBUG) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, settings.DCCSI_GDEBUG) - -for handler in _logging.root.handlers[:]: - _logging.root.removeHandler(handler) - -_MODULENAME = 'DCCsi.SDK.pythontools.launcher.main' - -_log_level = _logging.INFO -if _G_DEBUG: - _log_level = _logging.DEBUG - -_LOGGER = azpy.initialize_logger(name=_MODULENAME, - log_to_file=True, - default_log_level=_log_level) - -_LOGGER.debug('Starting up: {0}.'.format({_MODULENAME})) -# ------------------------------------------------------------------------- - - -# ------------------------------------------------------------------------- -def main(): - from PySide2.QtWidgets import QApplication, QPushButton - - app = QApplication(sys.argv) -# ------------------------------------------------------------------------- - - -# -------------------------------------------------------------------------- -if __name__ == '__main__': - """Run this file as main""" - -app = QApplication([]) # Start an application. -window = QWidget() # Create a window. -layout = QVBoxLayout() # Create a layout. -button = QPushButton("I'm just a Button man") # Define a button -layout.addWidget(QLabel('Hello World!')) # Add a label -layout.addWidget(button) # Add the button man -window.setLayout(layout) # Pass the layout to the window -window.show() # Show window -app.exec_() # Execute the App diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/atom_material.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/atom_material.py index 4c35861b9c..b7a6bfa3a1 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/atom_material.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/atom_material.py @@ -29,7 +29,7 @@ from pathlib import Path # ------------------------------------------------------------------------- # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/bootstrap.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/bootstrap.py index 50a4d3fdda..f3e91b59ce 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/bootstrap.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/bootstrap.py @@ -34,20 +34,20 @@ from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE # these are for module debugging, set to false on submit -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = 'DCCsi.SDK.substance.builder.bootstrap' _log_level = int(20) -if _G_DEBUG: +if _DCCSI_GDEBUG: _log_level = int(10) _LOGGER = azpy.initialize_logger(_PACKAGENAME, log_to_file=True, default_log_level=_log_level) _LOGGER.debug('Starting up: {0}.'.format({_PACKAGENAME})) _LOGGER.debug('_DCCSIG_PATH: {}'.format(_DCCSIG_PATH)) -_LOGGER.debug('_G_DEBUG: {}'.format(_G_DEBUG)) +_LOGGER.debug('_G_DEBUG: {}'.format(_DCCSI_GDEBUG)) _LOGGER.debug('_DCCSI_DEV_MODE: {}'.format(_DCCSI_DEV_MODE)) if _DCCSI_DEV_MODE: @@ -69,7 +69,7 @@ from dynaconf import settings try: from PySide2.QtWidgets import QApplication except: - _dccsi_config.init_ly_pyside(settings.LY_DEV) # init for standalone + _dccsi_config.init_o3de_pyside(settings.O3DE_DEV) # init for standalone # running in the editor if the QtForPython Gem is enabled # you should already have access and shouldn't need to set up @@ -92,20 +92,20 @@ os.environ["PYSBS_DIR_PATH"] = str(_PYSBS_DIR_PATH) # standard paths we may use downstream # To Do: move these into a dynaconf config extension specific to this tool? -from azpy.constants import ENVAR_LY_DEV -_LY_DEV = Path(os.getenv(ENVAR_LY_DEV, - settings.LY_DEV)).resolve() +from azpy.constants import ENVAR_O3DE_DEV +_O3DE_DEV = Path(os.getenv(ENVAR_O3DE_DEV, + settings.O3DE_DEV)).resolve() -from azpy.constants import ENVAR_LY_PROJECT_PATH -_LY_PROJECT_PATH = Path(os.getenv(ENVAR_LY_PROJECT_PATH, - settings.LY_PROJECT_PATH)).resolve() +from azpy.constants import ENVAR_O3DE_PROJECT_PATH +_O3DE_PROJECT_PATH = Path(os.getenv(ENVAR_O3DE_PROJECT_PATH, + settings.O3DE_PROJECT_PATH)).resolve() from azpy.constants import ENVAR_DCCSI_SDK_PATH _DCCSI_SDK_PATH = Path(os.getenv(ENVAR_DCCSI_SDK_PATH, settings.DCCSIG_SDK_PATH)).resolve() # build some reuseable path parts for the substance builder -_PROJECT_ASSETS_PATH = Path(_LY_PROJECT_PATH, 'Assets').resolve() +_PROJECT_ASSETS_PATH = Path(_O3DE_PROJECT_PATH, 'Assets').resolve() _PROJECT_MATERIALS_PATH = Path(_PROJECT_ASSETS_PATH, 'Materials').resolve() # ------------------------------------------------------------------------- @@ -116,15 +116,15 @@ _PROJECT_MATERIALS_PATH = Path(_PROJECT_ASSETS_PATH, 'Materials').resolve() if __name__ == "__main__": """Run this file as main""" - _LOGGER.info('_LY_DEV: {}'.format(_LY_DEV)) - _LOGGER.info('_LY_PROJECT_PATH: {}'.format(_LY_PROJECT_PATH)) + _LOGGER.info('_O3DE_DEV: {}'.format(_O3DE_DEV)) + _LOGGER.info('_O3DE_PROJECT_PATH: {}'.format(_O3DE_PROJECT_PATH)) _LOGGER.info('_DCCSI_SDK_PATH: {}'.format(_DCCSI_SDK_PATH)) _LOGGER.info('_PYSBS_DIR_PATH: {}'.format(_PYSBS_DIR_PATH)) _LOGGER.info('_PROJECT_ASSETS_PATH: {}'.format(_PROJECT_ASSETS_PATH)) _LOGGER.info('_PROJECT_MATERIALS_PATH: {}'.format(_PROJECT_MATERIALS_PATH)) - if _G_DEBUG: + if _DCCSI_GDEBUG: _dccsi_config.test_pyside2() # runs a small PySdie2 test # remove the logger diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sb_gui_main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sb_gui_main.py index cc501fe872..9408b94919 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sb_gui_main.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sb_gui_main.py @@ -35,7 +35,7 @@ from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, settings.DCCSI_GDEBUG) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, settings.DCCSI_GDEBUG) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, settings.DCCSI_GDEBUG) for handler in _logging.root.handlers[:]: @@ -44,7 +44,7 @@ for handler in _logging.root.handlers[:]: _MODULENAME = 'DCCsi.SDK.substance.builder.sb_gui_main' _log_level = _logging.INFO -if _G_DEBUG: +if _DCCSI_GDEBUG: _log_level = _logging.DEBUG _LOGGER = azpy.initialize_logger(name=_MODULENAME, @@ -71,12 +71,12 @@ import config _LOGGER.debug('config.py is: {}'.format(config)) # initialize the Lumberyard Qt / PySide2 -config.init_ly_pyside(settings.LY_DEV) # for standalone +config.init_o3de_pyside(settings.O3DE_DEV) # for standalone settings.setenv() # for standalone # log debug info about Qt/PySide2 _LOGGER.debug('QTFORPYTHON_PATH: {}'.format(settings.QTFORPYTHON_PATH)) -_LOGGER.debug('LY_BIN_PATH: {}'.format(settings.LY_BIN_PATH)) +_LOGGER.debug('O3DE_BIN_PATH: {}'.format(settings.O3DE_BIN_PATH)) _LOGGER.debug('QT_PLUGIN_PATH: {}'.format(settings.QT_PLUGIN_PATH)) _LOGGER.debug('QT_QPA_PLATFORM_PLUGIN_PATH: {}'.format(settings.QT_QPA_PLATFORM_PLUGIN_PATH)) # ------------------------------------------------------------------------- @@ -123,26 +123,26 @@ from atom_material import AtomMaterial # ------------------------------------------------------------------------- # To Do: still should manage via dynaconf (dynamic config and settings) -from azpy.constants import ENVAR_LY_DEV -_LY_DEV = Path(os.getenv(ENVAR_LY_DEV, None)).resolve() +from azpy.constants import ENVAR_O3DE_DEV +_O3DE_DEV = Path(os.getenv(ENVAR_O3DE_DEV, None)).resolve() -from azpy.constants import ENVAR_LY_PROJECT -_LY_PROJECT = os.getenv(ENVAR_LY_PROJECT, None) +from azpy.constants import ENVAR_O3DE_PROJECT +_O3DE_PROJECT = os.getenv(ENVAR_O3DE_PROJECT, None) -from azpy.constants import ENVAR_LY_PROJECT_PATH -_LY_PROJECT_PATH = Path(os.getenv(ENVAR_LY_PROJECT_PATH, None)).resolve() +from azpy.constants import ENVAR_O3DE_PROJECT_PATH +_O3DE_PROJECT_PATH = Path(os.getenv(ENVAR_O3DE_PROJECT_PATH, None)).resolve() from azpy.constants import ENVAR_DCCSI_SDK_PATH _DCCSI_SDK_PATH = Path(os.getenv(ENVAR_DCCSI_SDK_PATH, None)).resolve() # build some reuseable path parts -_PROJECT_ASSET_PATH = Path(_LY_PROJECT_PATH).resolve() -_PROJECT_ASSETS_PATH = Path(_LY_PROJECT_PATH, 'Materials').resolve() +_PROJECT_ASSET_PATH = Path(_O3DE_PROJECT_PATH).resolve() +_PROJECT_ASSETS_PATH = Path(_O3DE_PROJECT_PATH, 'Materials').resolve() # To Do: figure out a proper way to deal with Lumberyard game projects -_GEM_MATPLAY_PATH = Path(_LY_DEV, 'Gems', 'AtomContent', 'AtomMaterialPlayground').resolve() -_GEM_ROYALTYFREE = Path(_LY_DEV, 'Gems', 'AtomContent', 'RoyaltyFreeAssets').resolve() -_GEM_SUBSOURCELIBRARY = Path(_LY_DEV, 'Gems', 'AtomContent', 'SubstanceSourceLibrary').resolve() +_GEM_MATPLAY_PATH = Path(_O3DE_DEV, 'Gems', 'AtomContent', 'AtomMaterialPlayground').resolve() +_GEM_ROYALTYFREE = Path(_O3DE_DEV, 'Gems', 'AtomContent', 'RoyaltyFreeAssets').resolve() +_GEM_SUBSOURCELIBRARY = Path(_O3DE_DEV, 'Gems', 'AtomContent', 'SubstanceSourceLibrary').resolve() _SUB_LIBRARY_PATH = Path(_GEM_SUBSOURCELIBRARY, 'Assets', 'SubstanceSource', 'Library').resolve() # ^ This hard codes a bunch of known asset gems, again bad # To Do: figure out a proper way to scrap the gem registry from project @@ -150,9 +150,9 @@ _SUB_LIBRARY_PATH = Path(_GEM_SUBSOURCELIBRARY, 'Assets', 'SubstanceSource', 'Li # path to watcher script _WATCHER_SCRIPT_PATH = Path(_DCCSI_SDK_PATH, 'substance', 'builder', 'watchdog', '__init__.py').resolve() -_TEX_RNDR_PATH = Path(_LY_PROJECT_PATH, 'Materials', 'Substance').resolve() -_MAT_OUTPUT_PATH = Path(_LY_PROJECT_PATH, 'Materials', 'Substance').resolve() -_SBSAR_COOK_PATH = Path(_LY_PROJECT_PATH, 'Materials', 'Substance').resolve() +_TEX_RNDR_PATH = Path(_O3DE_PROJECT_PATH, 'Materials', 'Substance').resolve() +_MAT_OUTPUT_PATH = Path(_O3DE_PROJECT_PATH, 'Materials', 'Substance').resolve() +_SBSAR_COOK_PATH = Path(_O3DE_PROJECT_PATH, 'Materials', 'Substance').resolve() # ------------------------------------------------------------------------- @@ -171,7 +171,7 @@ class Window(QtWidgets.QDialog): # we should really init non-Qt stuff and set things up as properties if project_path is None: - self.project_path = str(_LY_PROJECT_PATH) + self.project_path = str(_O3DE_PROJECT_PATH) else: self.project_path = Path(project_path) @@ -213,7 +213,7 @@ class Window(QtWidgets.QDialog): self.matOutputPathComboBox = self.createComboBox(str(_MAT_OUTPUT_PATH)) # self.directoryComboBox = self.createComboBox(QtCore.QDir.currentPath()) - # I changed this to scan the _LY_PROJECT + # I changed this to scan the _O3DE_PROJECT # self.sbsarDirectory = self.return_1st_sbsar(Path(self.project_path, 'Assets')).resolve().parent self.sbsarDirectory = QtCore.QDir() self.sbsarDirectory.setCurrent(str(_PROJECT_ASSET_PATH)) @@ -672,13 +672,13 @@ class Window(QtWidgets.QDialog): # if you want relative paths here is a better way # first of all, assume we know the project we are in - #_LY_PROJECT_PATH + #_O3DE_PROJECT_PATH texture_output_path = Path(self.texRenderPathComboBox.currentText()).resolve() rel_tex_path = None for p in texture_output_path.parts: - if _LY_PROJECT == p: - index = texture_output_path.parts.index(_LY_PROJECT) + if _O3DE_PROJECT == p: + index = texture_output_path.parts.index(_O3DE_PROJECT) rel_tuple = texture_output_path.parts[index + 1:] rel_tex_path = Path(*list(rel_tuple)) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbs_to_sbsar.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbs_to_sbsar.py index 4763bf430c..441110b8d4 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbs_to_sbsar.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbs_to_sbsar.py @@ -47,7 +47,7 @@ import pysbs.context as pysbs_context # ------------------------------------------------------------------------- # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ @@ -68,10 +68,10 @@ _SYNTH_ENV_DICT = OrderedDict() _SYNTH_ENV_DICT = azpy.synthetic_env.stash_env(_SYNTH_ENV_DICT) # grab a specific path from the base_env _PATH_DCCSI = _SYNTH_ENV_DICT[ENVAR_DCCSIG_PATH] -_LY_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_LY_PROJECT_PATH] +_O3DE_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_O3DE_PROJECT_PATH] # build some reuseable path parts -_PATH_MOCK_ASSETS = Path(_LY_PROJECT_PATH, 'Assets').norm() +_PATH_MOCK_ASSETS = Path(_O3DE_PROJECT_PATH, 'Assets').norm() _PATH_MOCK_SUBLIB = Path(_PATH_MOCK_ASSETS, 'SubstanceSource').norm() _PATH_MOCK_SBS = Path(_PATH_MOCK_SUBLIB, 'sbs').norm() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_info.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_info.py index 2a26f13be2..9652b8990a 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_info.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_info.py @@ -47,7 +47,7 @@ import pysbs.context as pysbs_context # ------------------------------------------------------------------------- # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ @@ -62,7 +62,7 @@ _LOGGER.debug('Starting up: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- # global space debug flag -_G_DEBUG = os.getenv(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = os.getenv(ENVAR_DCCSI_GDEBUG, False) # global space debug flag _DCCSI_DEV_MODE = os.getenv(ENVAR_DCCSI_DEV_MODE, False) @@ -88,10 +88,10 @@ _SYNTH_ENV_DICT = OrderedDict() _SYNTH_ENV_DICT = azpy.synthetic_env.stash_env(_SYNTH_ENV_DICT) # grab a specific path from the base_env _PATH_DCCSI = _SYNTH_ENV_DICT[ENVAR_DCCSIG_PATH] -_LY_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_LY_PROJECT_PATH] +_O3DE_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_O3DE_PROJECT_PATH] # build some reuseable path parts -_PATH_MOCK_ASSETS = Path(_LY_PROJECT_PATH, 'Assets').norm() +_PATH_MOCK_ASSETS = Path(_O3DE_PROJECT_PATH, 'Assets').norm() _PATH_MOCK_SUBLIB = Path(_PATH_MOCK_ASSETS, 'SubstanceSource').norm() _PATH_MOCK_SBS = Path(_PATH_MOCK_SUBLIB, 'sbs').norm() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_render.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_render.py index 31e2c81a57..6285ea0335 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_render.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_render.py @@ -45,7 +45,7 @@ import pysbs.context as pysbs_context # ------------------------------------------------------------------------- # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ @@ -66,10 +66,10 @@ _SYNTH_ENV_DICT = OrderedDict() _SYNTH_ENV_DICT = azpy.synthetic_env.stash_env(_SYNTH_ENV_DICT) # grab a specific path from the base_env _PATH_DCCSI = _SYNTH_ENV_DICT[ENVAR_DCCSIG_PATH] -_LY_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_LY_PROJECT_PATH] +_O3DE_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_O3DE_PROJECT_PATH] # build some reuseable path parts -_PATH_MOCK_ASSETS = Path(_LY_PROJECT_PATH, 'Assets').norm() +_PATH_MOCK_ASSETS = Path(_O3DE_PROJECT_PATH, 'Assets').norm() _PATH_MOCK_SUBLIB = Path(_PATH_MOCK_ASSETS, 'SubstanceSource').norm() _PATH_MOCK_SBS = Path(_PATH_MOCK_SUBLIB, 'sbs').norm() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_utils.py index 74293a8d77..8ffdf30e35 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_utils.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_utils.py @@ -28,7 +28,7 @@ from azpy.constants import ENVAR_DCCSI_DEV_MODE from dynaconf import settings from pathlib import Path -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, settings.DCCSI_GDEBUG) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, settings.DCCSI_GDEBUG) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, settings.DCCSI_DEV_MODE) _MODULENAME = 'DCCsi.SDK.substance.builder.sbsar_utils' @@ -190,15 +190,15 @@ if __name__ == "__main__": _SYNTH_ENV_DICT = synthetic_env.stash_env() from azpy.constants import ENVAR_DCCSIG_PATH - from azpy.constants import ENVAR_LY_PROJECT_PATH + from azpy.constants import ENVAR_O3DE_PROJECT_PATH # grab a specific path from the base_env _PATH_DCCSI = _SYNTH_ENV_DICT[ENVAR_DCCSIG_PATH] # use DCCsi as the project path for this test - _LY_PROJECT_PATH = _PATH_DCCSI + _O3DE_PROJECT_PATH = _PATH_DCCSI - _PROJECT_ASSETS_PATH = Path(_LY_PROJECT_PATH, 'Assets').resolve() + _PROJECT_ASSETS_PATH = Path(_O3DE_PROJECT_PATH, 'Assets').resolve() _PROJECT_MATERIALS_PATH = Path(_PROJECT_ASSETS_PATH, 'Materials').resolve() # this will combine two parts into a single path (object) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/substance_tools.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/substance_tools.py index af001902ed..0407a1db08 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/substance_tools.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/substance_tools.py @@ -46,7 +46,7 @@ import pysbs.context as pysbs_context # ------------------------------------------------------------------------- # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ @@ -67,10 +67,10 @@ _SYNTH_ENV_DICT = OrderedDict() _SYNTH_ENV_DICT = azpy.synthetic_env.stash_env(_SYNTH_ENV_DICT) # grab a specific path from the base_env _PATH_DCCSI = _SYNTH_ENV_DICT[ENVAR_DCCSIG_PATH] -_LY_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_LY_PROJECT_PATH] +_O3DE_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_O3DE_PROJECT_PATH] # build some reuseable path parts -_PATH_MOCK_ASSETS = Path(_LY_PROJECT_PATH, 'Assets').norm() +_PATH_MOCK_ASSETS = Path(_O3DE_PROJECT_PATH, 'Assets').norm() _PATH_MOCK_SUBLIB = Path(_PATH_MOCK_ASSETS, 'SubstanceSource').norm() _PATH_MOCK_SBS = Path(_PATH_MOCK_SUBLIB, 'sbs').norm() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/watchdog/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/watchdog/__init__.py index d37a9cb5ec..35c970e711 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/watchdog/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/watchdog/__init__.py @@ -53,7 +53,7 @@ import pysbs.context as pysbs_context # ------------------------------------------------------------------------- # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ @@ -71,8 +71,8 @@ _LOGGER.debug('Starting up: {0}.'.format({_PACKAGENAME})) from collections import OrderedDict _SYNTH_ENV_DICT = OrderedDict() _SYNTH_ENV_DICT = azpy.synthetic_env.stash_env(_SYNTH_ENV_DICT) -_LY_DEV = _SYNTH_ENV_DICT[ENVAR_LY_DEV] -_LY_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_LY_PROJECT_PATH] +_O3DE_DEV = _SYNTH_ENV_DICT[ENVAR_O3DE_DEV] +_O3DE_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_O3DE_PROJECT_PATH] # ------------------------------------------------------------------------- @@ -90,7 +90,7 @@ class MyHandler(PatternMatchingEventHandler): """ self.outputName = event.src_path.split(".sbsar")[0].split("/")[-1] self.outputCookPath = event.src_path.split(self.outputName) - self.outputRenderPath = Path(_LY_PROJECT_PATH, 'Assets', 'Textures', 'Substance').norm() + self.outputRenderPath = Path(_O3DE_PROJECT_PATH, 'Assets', 'Textures', 'Substance').norm() _LOGGER.debug(self.outputCookPath, self.outputName, self.outputRenderPath) pysbs_batch.sbsrender_info(input=event.src_path) diff --git a/AutomatedTesting/Gem/PythonTests/editor_test_testing/conftest.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/3dsMax/stub similarity index 65% rename from AutomatedTesting/Gem/PythonTests/editor_test_testing/conftest.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/3dsMax/stub index 1f49f7111b..d365a5f2c9 100644 --- a/AutomatedTesting/Gem/PythonTests/editor_test_testing/conftest.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/3dsMax/stub @@ -1,8 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python """ 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 """ - -pytest_plugins = ["pytester"] +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/MaterialExporter/_init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/MaterialExporter/_init__.py new file mode 100644 index 0000000000..d365a5f2c9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/MaterialExporter/_init__.py @@ -0,0 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/MaterialExporter/main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/MaterialExporter/main.py new file mode 100644 index 0000000000..b08991f63e --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/MaterialExporter/main.py @@ -0,0 +1,21 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- +# DCCsi\\Tools\\Blender\\AddOns\\MaterialExporter\\main.py + +""" A in Blender tool for exporting BRDF materials as O3DE Atom StandardPBR +""" + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + """Run this file as main""" + + print('MaterialExporter.main() not implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/config.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/config.py new file mode 100644 index 0000000000..b0f67b1034 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/config.py @@ -0,0 +1,11 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- + +print('Not Implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.dev/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/settings.json similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.dev/stub rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/settings.json diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/start.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/start.py new file mode 100644 index 0000000000..b0f67b1034 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/start.py @@ -0,0 +1,11 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- + +print('Not Implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Houdini/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Houdini/stub new file mode 100644 index 0000000000..d365a5f2c9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Houdini/stub @@ -0,0 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Marmoset/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Marmoset/stub new file mode 100644 index 0000000000..d365a5f2c9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Marmoset/stub @@ -0,0 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/Prefs/icons/MayaStartupImage.png b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/Prefs/icons/MayaStartupImage.png new file mode 100644 index 0000000000..8f5d8290a8 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/Prefs/icons/MayaStartupImage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:083ab199e273431963fc5f80bb80b7d1b1d428f7b12a5180d7199a7431291982 +size 311865 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/plugins/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/plugins/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/scripts/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/scripts/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/siteDir/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/siteDir/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Help/HelpStub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Help/HelpStub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Projects/default/workspace.mel b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Projects/default/workspace.mel new file mode 100644 index 0000000000..c6474c4414 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Projects/default/workspace.mel @@ -0,0 +1,36 @@ +//Maya 2016 Project Definition + +workspace -fr "fluidCache" "cache/nCache/fluid"; +workspace -fr "images" "images"; +workspace -fr "offlineEdit" "scenes/edits"; +workspace -fr "furShadowMap" "renderData/fur/furShadowMap"; +workspace -fr "iprImages" "renderData/iprImages"; +workspace -fr "renderData" "renderData"; +workspace -fr "scripts" "scripts"; +workspace -fr "fileCache" "cache/nCache"; +workspace -fr "eps" "data"; +workspace -fr "shaders" "renderData/shaders"; +workspace -fr "3dPaintTextures" "sourceimages/3dPaintTextures"; +workspace -fr "translatorData" "data"; +workspace -fr "mel" "scripts"; +workspace -fr "furFiles" "renderData/fur/furFiles"; +workspace -fr "OBJ" "data"; +workspace -fr "particles" "cache/particles"; +workspace -fr "scene" "scenes"; +workspace -fr "furEqualMap" "renderData/fur/furEqualMap"; +workspace -fr "sourceImages" "sourceimages"; +workspace -fr "furImages" "renderData/fur/furImages"; +workspace -fr "clips" "clips"; +workspace -fr "depth" "renderData/depth"; +workspace -fr "movie" "movies"; +workspace -fr "audio" "sound"; +workspace -fr "bifrostCache" "cache/bifrost"; +workspace -fr "autoSave" "autosave"; +workspace -fr "mayaAscii" "scenes"; +workspace -fr "move" "data"; +workspace -fr "sound" "sound"; +workspace -fr "diskCache" "data"; +workspace -fr "illustrator" "data"; +workspace -fr "mayaBinary" "scenes"; +workspace -fr "templates" "assets"; +workspace -fr "furAttrMap" "renderData/fur/furAttrMap"; diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/IBLbaker_brdf_lut.dds b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/IBLbaker_brdf_lut.dds new file mode 100644 index 0000000000..81e62781b4 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/IBLbaker_brdf_lut.dds @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:646b6d93b2c672bbbbcb46af1bfcaf26ca37c8a0c2b218989b145417bb6b7c93 +size 262272 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_lighting_diffuse.dds b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_lighting_diffuse.dds new file mode 100644 index 0000000000..980477af4b --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_lighting_diffuse.dds @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86209db0389b152c709d529e9d5705219b44bfbf712a26788a5c2ab0adc0c373 +size 98452 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_lighting_specular.dds b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_lighting_specular.dds new file mode 100644 index 0000000000..fd6e1f99fc --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_lighting_specular.dds @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd95898a04b81b80b095dbef34523f3b70a8c14bc9f82116f732ab648f25658b +size 2096788 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_skybox.dds b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_skybox.dds new file mode 100644 index 0000000000..304aaa0a1f --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_skybox.dds @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a85a15d5c60102f414b4ad03604dbd1c78e3d1c1fe445f60db158b2c069f792d +size 25165972 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/SourceImages/MayaStartupImage.psd b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/SourceImages/MayaStartupImage.psd new file mode 100644 index 0000000000..8d630620ca --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/SourceImages/MayaStartupImage.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1318f73ca32ec56dfeb0233679504f6fc723081f0cafa8e7e2d0517b878defd1 +size 2000893 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/SourceImages/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/SourceImages/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/workspace.mel b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/workspace.mel new file mode 100644 index 0000000000..2cbf02f1aa --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/workspace.mel @@ -0,0 +1,69 @@ +//Maya 2016 Project Definition + +workspace -fr "fluidCache" "mayaData/cache/nCache/fluid"; +workspace -fr "JT_DC" "mayaData/Trans/JT"; +workspace -fr "CATIAV4_DC" "mayaData/Trans/CATIAV4"; +workspace -fr "images" "mayaData/Images"; +workspace -fr "offlineEdit" "ArtSource/Maya"; +workspace -fr "STEP_DC" "mayaData/Trans/STEP"; +workspace -fr "furShadowMap" "mayaData/renderData/fur/furShadowMap"; +workspace -fr "SPF_DCE" "mayaData/Trans/SPF"; +workspace -fr "scripts" "mayaData/Scripts"; +workspace -fr "CATIAV5_DC" "mayaData/Trans/CATIAV5"; +workspace -fr "DAE_FBX" "mayaData/Trans/DAE_FBX"; +workspace -fr "shaders" "mayaData/renderData/shaders"; +workspace -fr "furFiles" "mayaData/renderData/fur/furFiles"; +workspace -fr "OBJ" "mayaData/OBJ"; +workspace -fr "FBX export" "mayaData/Trans/FBX_export"; +workspace -fr "furEqualMap" "mayaData/renderData/fur/furEqualMap"; +workspace -fr "Autodesk Packet File" "mayaData/Trans"; +workspace -fr "DAE_FBX export" "mayaData/Trans/DAE_FBX_export"; +workspace -fr "SPF_DC" "mayaData/Trans/SPF"; +workspace -fr "movie" "mayaData/movies"; +workspace -fr "DXF_DCE" "mayaData/Trans/DXF"; +workspace -fr "move" "mayaData/move"; +workspace -fr "mayaAscii" "ArtSource"; +workspace -fr "autoSave" "mayaData"; +workspace -fr "sound" "mayaData/Sounds"; +workspace -fr "mayaBinary" "ArtSource"; +workspace -fr "ZPR_DCE" "mayaData/Trans/ZPR"; +workspace -fr "STL_DCE" "mayaData/Trans/STL"; +workspace -fr "iprImages" "mayaData/renderData/iprImages"; +workspace -fr "PhysX" "mayaData/Trans/Physx"; +workspace -fr "DXF_DC" "mayaData/Trans/DXF"; +workspace -fr "FBX" "mayaData/Trans/FBX"; +workspace -fr "studioImport" "mayaData/Trans"; +workspace -fr "UG_DCE" "mayaData/Trans/UG"; +workspace -fr "renderData" "mayaData/renderData"; +workspace -fr "fileCache" "mayaData/cache/nCache"; +workspace -fr "eps" "mayaData/EPS"; +workspace -fr "Fbx" "Objects"; +workspace -fr "3dPaintTextures" "mayaData/images/3dPaintTextures"; +workspace -fr "translatorData" "mayaData"; +workspace -fr "mel" "mayaData/Scripts/Mel"; +workspace -fr "particles" "mayaData/cache/particles"; +workspace -fr "IV_DC" "mayaData/Trans/IV"; +workspace -fr "scene" "ArtSource"; +workspace -fr "DWG_DCE" "mayaData/Trans/DWG"; +workspace -fr "MayaCryExport" "Objects"; +workspace -fr "sourceImages" "ArtSource/Textures"; +workspace -fr "furImages" "mayaData/renderData/fur/furImages"; +workspace -fr "clips" "mayaData/clips"; +workspace -fr "PTC_DC" "mayaData/Trans/PTC"; +workspace -fr "STL_DC" "mayaData/Trans/STL"; +workspace -fr "IPT_DC" "mayaData/Trans/IPT"; +workspace -fr "CSB_DC" "mayaData/Trans/CSB"; +workspace -fr "SW_DC" "mayaData/Trans/SW"; +workspace -fr "depth" "mayaData/renderData/depth"; +workspace -fr "audio" "mayaData/Sounds"; +workspace -fr "DWG_DC" "mayaData/Trans/DWG"; +workspace -fr "bifrostCache" "mayaData/cache/bifrost"; +workspace -fr "IGES_DCE" "mayaData/Trans/IGES"; +workspace -fr "Alembic" "mayaData/Trans/Alembic"; +workspace -fr "illustrator" "mayaData/AI"; +workspace -fr "diskCache" "mayaData"; +workspace -fr "UG_DC" "mayaData/Trans/UG"; +workspace -fr "templates" "mayaData/assets"; +workspace -fr "OBJexport" "mayaData/Trans/Obj"; +workspace -fr "furAttrMap" "mayaData/renderData/fur/furAttrMap"; +workspace -fr "IGES_DC" "mayaData/Trans/IGES"; diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Mel/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Mel/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/stub new file mode 100644 index 0000000000..d365a5f2c9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/stub @@ -0,0 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/stub_util.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/stub_util.py new file mode 100644 index 0000000000..1dc43d8485 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/stub_util.py @@ -0,0 +1,12 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- + +print('Not Implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/constants.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/constants.py new file mode 100644 index 0000000000..6ed8297ab9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/constants.py @@ -0,0 +1,32 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- + +""" +Module Documentation: + DccScriptingInterface:: Tools//maya//scripts//constants.py + +This module is mainly a bunch of commony used constants, and default strings +So we can make an update here once that is used elsewhere +""" +# ------------------------------------------------------------------------- +# built-ins +# none + +# -- External Python modules + +# -- DCCsi Extension Modules +#import azpy + +# -- maya imports +# none +# ------------------------------------------------------------------------- +OBJ_DCCSI_MAINMENU = 'O3deDCCsiMainMenu' +TAG_DCCSI_MAINMENU = 'DCCsi (O3DE:Atom)' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_callbacks.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_callbacks.py new file mode 100644 index 0000000000..4a2b756a5e --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_callbacks.py @@ -0,0 +1,201 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- +""" +Module Documentation: + DccScriptingInterface:: SDK//maya//scripts//set_callbacks.py + +This module manages a set of predefined callbacks for maya +""" +# ------------------------------------------------------------------------- +# -- Standard Python modules +import os +import sys +import logging as _logging +# -- External Python modules +from box import Box +# maya imports +import maya.cmds as mc +import maya.api.OpenMaya as om +# -- DCCsi Extension Modules +from azpy.constants import * +import azpy.dcc.maya +azpy.dcc.maya.init() # <-- should have already run? +import azpy.dcc.maya.callbacks.event_callback_handler as azEvCbH +import azpy.dcc.maya.callbacks.node_message_callback_handler as azNdMsH +# Node Message Callback Setup +import azpy.dcc.maya.callbacks.on_shader_rename as oSR +from set_defaults import set_defaults +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +from azpy.env_bool import env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG +from azpy.constants import ENVAR_DCCSI_DEV_MODE + +# global space +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, True) +_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, True) + +_MODULENAME = r'DCCsi.SDK.Maya.Scripts.set_callbacks' + +_LOGGER = azpy.initialize_logger(_MODULENAME, default_log_level=int(20)) +_LOGGER.debug('Invoking:: {0}.'.format({_MODULENAME})) +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# global scope callbacks, set up set and initialize all to None +# To Do: should callback initialization use data-driven settings? +# To Do: should we move callback initialization to a sub-module? +# To Do: move the callback key like 'NewSceneOpened' here (instead of None) +# ^ this would provide ability to loop through and replace key with CB object + +_G_CALLBACKS = Box(box_dots=True) # global scope container +_G_PRIMEKEY = 'DCCsi_callbacks' +_G_CALLBACKS[_G_PRIMEKEY] = True # required prime key + + +# ------------------------------------------------------------------------- +def init_callbacks(_callbacks=_G_CALLBACKS): + # store as a dict (Box is a fancy dict) + _callbacks[_G_PRIMEKEY] = True # required prime key + + # signature dict['callback key'] = ('CallBack'(type), func, callbackObj) + _callbacks['on_new_file'] = ['NewSceneOpened', set_defaults, None] + _callbacks['new_scene_fix_paths'] = ['NewSceneOpened', install_fix_paths, None] + _callbacks['post_scene_fix_paths'] = ['PostSceneRead', install_fix_paths, None] + _callbacks['workspace_changed'] = ['workspaceChanged', update_workspace, None] + _callbacks['quit_app'] = ['quitApplication', uninstall_callbacks, None] + + # nodeMessage style callbacks + # fire a function + _func_00 = oSR.on_shader_rename_rename_shading_group + # using a nodeMessage callback trigger + _cb_00 = om.MNodeMessage.addNameChangedCallback + # all nodeMessage type callbacks can use 'nodeMessageType' key + _callbacks['shader_rename'] = ['nodeMessageType', (_func_00, _cb_00), None] + + return _callbacks +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def uninstall_callbacks(): + """Bulk uninstalls hte globally defined set of callbacks: + _G_callbacks""" + + global _G_CALLBACKS + + _LOGGER.debug('uninstall_callbacks() fired') + + for key, value in _G_CALLBACKS: + if value[2] is not None: # have a cb + value[2].uninstall() # so uninstall it + else: + _LOGGER.warning('No callback in: key {0}, value:{1}' + ''.format(key, value)) + _G_CALLBACKS = None + _LOGGER.info('DCCSI CALLBACKS UNINSTALLED ... EXITING') + return _G_CALLBACKS +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def install_callbacks(_callbacks=_G_CALLBACKS): + """Bulk installs the globally defined set of callbacks: + _G_callbacks""" + + _LOGGER.debug('install_callback_set() fired') + + _callbacks = init_callbacks(_callbacks) + + # we initialized the box with this so pop it + if 'box_dots' in _callbacks: + _callbacks.pop('box_dots') + + # don't pass anything but carefully considered dict + if _G_PRIMEKEY in _callbacks: + _primekey = _callbacks.pop(_G_PRIMEKEY) + else: + _LOGGER.error('No prime key, use a correct dictionary') + #To Do: implement error handling and return codes + return _callbacks[None] + + for key, value in _G_CALLBACKS.items(): + # we popped the prime key should the rest should be safe + if value[0] != 'nodeMessageType': + # set callback up + _cb = azEvCbH.EventCallbackHandler(value[0], + value[1]) + # ^ installs by default + # stash it back into managed dict + value[2] = _cb + # value[2].install() + else: + # set up callback, value[1] should be tupple(func, trigger) + _cb = azNdMsH.NodeMessageCallbackHandler(value[1][0], + value[1][1]) + # ^ installs by default + # stash it back into managed dict + value[2] = _cb + # value[2].install() + + return _callbacks +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def install_fix_paths(foo=None): + """Installs and triggers a fix paths module. + This can repair broken reference paths in shaders""" + global _fix_paths + _fix_paths = None + + _LOGGER.debug('install_fix_paths() fired') + + # if we don't have it already, this function is potentially triggered + # by a callback, so we don't need to keep importing it. + try: + _fix_paths + reload(_fix_paths) + except Exception as e: + try: + import fixPaths as _fix_paths + except Exception as e: + # To Do: not implemented yet + _LOGGER.warning('NOT IMPLEMENTED: {0}'.format(e)) + + # if we have it, use it + if _fix_paths: + return _fix_paths.main() + else: + # To Do: implement error handling and return codes + return 1 +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def update_workspace(foo=None): + """Forces and update of the workspace (workspace.mel)""" + _LOGGER.debug('update_workspace() fired') + result = mc.workspace(update=True) + return result +# ------------------------------------------------------------------------- + +# install and init callbacks on an import obj +_G_CALLBACKS = install_callbacks(_G_CALLBACKS) + +# ========================================================================== +# Module Tests +#========================================================================== +if __name__ == '__main__': + _G_CALLBACKS = install_callbacks(_G_CALLBACKS) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_defaults.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_defaults.py new file mode 100644 index 0000000000..639b03c297 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_defaults.py @@ -0,0 +1,93 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- +""" +Module Documentation: + DccScriptingInterface:: SDK//maya//scripts//set_pref_defaults.py + +This module manages a predefined set of prefs for maya +""" +# ------------------------------------------------------------------------- +# -- Standard Python modules +import os +import sys +# -- External Python modules + +# -- DCCsi Extension Modules +import azpy +from azpy.constants import * + +# -- maya imports +import maya.cmds as mc +import maya.mel as mm + +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +from azpy.env_bool import env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG +from azpy.constants import ENVAR_DCCSI_DEV_MODE + +# global space +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) + +_MODULENAME = r'DCCsi.SDK.Maya.Scripts.set_defaults' + +_LOGGER = azpy.initialize_logger(_MODULENAME, default_log_level=int(20)) +_LOGGER.debug('Invoking:: {0}.'.format({_MODULENAME})) +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def set_defaults(units='meter'): + """This method will make defined settings changes to Maya prefs, + to better configure maya to work with Lumberyard""" + # To Do: make this data-driven env/settings, game teams should be able + # to opt out and/or set their prefered configuration. + + _LOGGER.debug('set_defaults_lumberyard() fired') + + # set up default units ... this should be moved to bootstrap config + _LOGGER.info('Default, 1 Linear Game Unit in Lumberyard == 1 Meter' + ' in Maya content. Setting default linear units to Meters' + ' (user can change to other units in the preferences)') + + result = mc.currentUnit(linear=units) + + # set up grid defaults + _LOGGER.info('Setting Grid defaults, to match default unit scale.' + '(user can change grid config manually') + try: + mc.grid(size=32, spacing=1, divisions=10) + except Exception as e: + _LOGGER.warning('{0}'.format(e)) + + # viewFit + _LOGGER.info('Changing default mc.viewFit') + try: + mc.viewFit() + except Exception as e: + _LOGGER.warning('{0}'.format(e)) + + # some mel commands + _LOGGER.info('Changing sersp camera clipping planes') + try: + mm.eval(str(r'setAttr "perspShape.nearClipPlane" 0.01;')) + mm.eval(str(r'setAttr "perspShape.farClipPlane" 1000;')) + except Exception as e: + _LOGGER.warning('{0}'.format(e)) + + # set up fixPaths + _LOGGER.info('~ Setting up fixPaths in default scene') + + return 0 +# ------------------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_menu.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_menu.py new file mode 100644 index 0000000000..e417324d0b --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_menu.py @@ -0,0 +1,88 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- +""" +Module Documentation: + DccScriptingInterface:: SDK//maya//scripts//set_menu.py + +This module creates and manages a DCCsi mainmenu +""" +# ------------------------------------------------------------------------- +# -- Standard Python modules +# none + +# -- External Python modules +# none + +# -- DCCsi Extension Modules +import azpy +from constants import OBJ_DCCSI_MAINMENU +from constants import TAG_DCCSI_MAINMENU + +# -- maya imports +import pymel.core as pm +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +from azpy.env_bool import env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG +from azpy.constants import ENVAR_DCCSI_DEV_MODE + +# global space +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) + +_MODULENAME = r'DCCsi.SDK.Maya.Scripts.set_menu' + +_LOGGER = azpy.initialize_logger(_MODULENAME, default_log_level=int(20)) +_LOGGER.debug('Invoking:: {0}.'.format({_MODULENAME})) +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def menu_cmd_test(): + _LOGGER.info('test_func(), is TESTING main menu') + return +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +def set_main_menu(obj_name=OBJ_DCCSI_MAINMENU, label=TAG_DCCSI_MAINMENU): + _main_window = pm.language.melGlobals['gMainWindow'] + + _menu_obj = obj_name + _menu_label = label + + # check if it already exists and remove (so we don't duplicate) + if pm.menu(_menu_obj, label=_menu_label, exists=True, parent=_main_window): + pm.deleteUI(pm.menu(_menu_obj, e=True, deleteAllItems=True)) + + # create the main menu object + _custom_tools_menu = pm.menu(_menu_obj, + label=_menu_label, + parent=_main_window, + tearOff=True) + + # make a dummpy sub-menu + pm.menuItem(label='Menu Item Stub', + subMenu=True, + parent=_custom_tools_menu, + tearOff=True) + + # make a dummy menu item to test + pm.menuItem(label='Test', command=pm.Callback(menu_cmd_test)) + return _custom_tools_menu + +# ========================================================================== +# Run as LICENSE +#========================================================================== +if __name__ == '__main__': + + _custom_menu = set_main_menu() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_shelf.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_shelf.py new file mode 100644 index 0000000000..1bee05afe8 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_shelf.py @@ -0,0 +1,130 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- +""" +Module Documentation: + DccScriptingInterface:: SDK//maya//scripts//set_shelf.py + +This module manages a custom shelf in maya for the DCCsi +Reference: https://gist.github.com/vshotarov/1c3176fe9e38dcaadd1e56c2f15c95d9 +""" +# ------------------------------------------------------------------------- +# -- Standard Python modules +# none + +# -- External Python modules +# none + +# -- DCCsi Extension Modules +# none + +# -- Maya Extension Modules +import maya.cmds as mc +# ------------------------------------------------------------------------- + +def _null(*args): + pass + +# ------------------------------------------------------------------------- +class customShelf(_Custom_Shelf): + '''This is an example shelf.''' + + def build(self): + self.add_button(label="button1") + self.add_button("button2") + self.add_button("popup") + p = mc.popupMenu(b=1) + self.add_menu_item(p, "popupMenuItem1") + self.add_menu_item(p, "popupMenuItem2") + sub = self.add_submenu(p, "subMenuLevel1") + self.add_menu_item(sub, "subMenuLevel1Item1") + sub2 = self.add_submenu(sub, "subMenuLevel2") + self.add_menu_item(sub2, "subMenuLevel2Item1") + self.add_menu_item(sub2, "subMenuLevel2Item2") + self.add_menu_item(sub, "subMenuLevel1Item2") + self.add_menu_item(p, "popupMenuItem3") + self.add_button("button3") +# ------------------------------------------------------------------------- + + + +class _Custom_Shelf(): + '''A simple class to build custom shelves in maya. + The build method is empty and an inheriting class should override''' + + def __init__(self, name="DCCsi", icon_path=""): + self._name = name + + self._icon_path = icon_path + + self._label_background_color = (0, 0, 0, 0) + self._label_colour = (.9, .9, .9) + + self._clean_old_shlef() + + mc.setParent(self._name) + + self.build() + + def build(self): + '''Override this method in custom class. + Otherwise, nothing is added to the shelf.''' + pass + + def add_button(self, + label='', + icon="commandButton.png", + command=_null, + doubleCommand=_null): + '''Adds a shelf button with the specified label, + command, double click command and image.''' + mc.setParent(self._name) + if icon: + icon = self._icon_path + icon + mc.shelfButton(width=37, height=37, + image=icon, + label=label, + command=command, + doubleClickCommand=doubleCommand, + imageOverlayLabel=label, + overlayLabelBackColor=self._label_background_color, + overlayLabelColor=self._label_colour) + + def add_menu_item(self, parent, label, command=_null, icon=""): + '''Adds a shelf button with the specified label, + command, double click command and image.''' + if icon: + icon = self._icon_path + icon + return mc.menuItem(p=parent, l=label, c=command, i="") + + def add_submenu(self, parent, label, icon=None): + '''Adds a sub menu item with the specified label and icon + to the specified parent popup menu.''' + if icon: + icon = self._icon_path + icon + return mc.menuItem(p=parent, l=label, i=icon, subMenu=1) + + def _clean_old_shlef(self): + '''Checks if the shelf exists and empties it if it does or + creates it if it does not.''' + if mc.shelfLayout(self._name, ex=1): + if mc.shelfLayout(self._name, q=1, ca=1): + for each in mc.shelfLayout(self._name, q=1, ca=1): + mc.deleteUI(each) + else: + mc.shelfLayout(self._name, p="ShelfLayout") + + +# ========================================================================== +# Module Tests +# ========================================================================== +if __name__ == '__main__': + customShelf() + pass diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/userSetup.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/userSetup.py new file mode 100644 index 0000000000..c68eb8d256 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/userSetup.py @@ -0,0 +1,325 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- +from __future__ import unicode_literals + +""" +This module fullfils the maya bootstrap pattern as described in their docs +https://tinyurl.com/y2aoz8es + +Pattern is similar to Lumberyard Editor\\Scripts\\bootstrap.py + +For now the proper way to initiate Maya boostrapping the DCCsi, is to use +the provided env and launcher bat files. + +If you are developing for the DCCsi you can use this launcher to start Maya: +DccScriptingInterface\\Launchers\\Windows\\Launch_Maya_2020.bat" + +To Do: ATOM-5861 +""" +__project__ = 'DccScriptingInterface' + +# it is really hard to debug userSetup bootstrapping +# this enables some rudimentary logging for debugging +_BOOT_INFO = True + +# ------------------------------------------------------------------------- +# built in's +import os +import sys +import site +import inspect +import traceback +import logging as _logging + +# -- DCCsi Extension Modules +import azpy +from azpy.constants import * +from azpy.env_bool import env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG +from azpy.constants import ENVAR_DCCSI_DEV_MODE + +# To Do: needs to be updated to use dynaconf and config.py +from azpy.env_base import _BASE_ENVVAR_DICT + +# -- maya imports +import maya.cmds as cmds +import maya.mel as mel +#from pymel.all import * +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# global space +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_DEV_MODE = True # force true for debugger testing + +_ORG_TAG = r'Amazon::Lumberyard' +_APP_TAG = r'DCCsi' +_TOOL_TAG = r'SDK.Maya.Scripts.userSetup' +_TYPE_TAG = r'entrypoint' # bootstrap + +_MODULENAME = str('{0}.{1}'.format(_APP_TAG, _TOOL_TAG)) + +_LOGGER = azpy.initialize_logger(_MODULENAME, default_log_level=int(20)) +_LOGGER.info('Initializing: {0}.'.format({_MODULENAME})) +_LOGGER.info('DCCSI_GDEBUG: {0}.'.format({_DCCSI_GDEBUG})) +_LOGGER.info('DCCSI_DEV_MODE: {0}.'.format({_DCCSI_DEV_MODE})) + +# flag to turn off setting up callbacks, until they are fully implemented +# To Do: consider making it a settings option to define and enable/disable +_G_LOAD_CALLBACKS = True # couple bugs, couple NOT IMPLEMENTED +_LOGGER.info('DCCSI_MAYA_SET_CALLBACKS: {0}.'.format({_G_LOAD_CALLBACKS})) + +# early attach WingIDE debugger (can refactor to include other IDEs later) +if _DCCSI_DEV_MODE: + from azpy.test.entry_test import connect_wing + foo = connect_wing() +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# To Do REMOVE this block and replace with dev module +# debug prints, To Do: this should be moved to bootstrap config +#_G_DEBUGGER = os.getenv(ENVAR_DCCSI_GDEBUGGER, "WING") + +#if _DCCSI_DEV_MODE: + #if _G_DEBUGGER == "WING": + #_LOGGER.info('{0}'.format('-' * 74)) + #_LOGGER.info('Developer Debug Mode: {0}, Basic debugger: {1}'.format(_G_DEBUG, _G_DEBUGGER)) + #try: + #_LOGGER.info('Attempting to start basic WING debugger') + #import azpy.lmbr.test + + #_LOGGER.info('Package Imported: azpy.test') + #ouput = azpy.entry_test.main(verbose=False, + #connectDebugger=True, + #returnOuput=_G_DEBUG) + #_LOGGER.info(ouput) + #pass + #except Exception as e: + #_LOGGER.info("Error: azpy.test, entry_test (didn't perform)") + #_LOGGER.info("Exception: {0}".format(e)) + #pass + #elif _G_DEBUGGER == "PYCHARM": + ## https://github.com/juggernate/PyCharm-Maya-Debugging + #_LOGGER.info('{0}'.format('-' * 74)) + #_LOGGER.info('Developer Debug Mode: {0}, Basic debugger: {1}'.format(_G_DEBUG, _G_DEBUGGER)) + #sys.path.append('C:\Program Files\JetBrains\PyCharm 2019.1.3\debug-eggs\pydevd-pycharm.egg') + #try: + #_LOGGER.info('Attempting to start basic PYCHARM debugger') + ## Inside Maya Python Console (Tip: add to a shelf button for quick access) + #import pydevd + + #_LOGGER.info('Package Imported: pydevd') + #pydevd.settrace('localhost', port=7720, suspend=False) + #_LOGGER.info('PYCHARM Debugger Attach Success!!!') + ## To disconnect run: + ## pydevd.stoptrace() + #pass + #except Exception as e: + #_LOGGER.info("Error: pydevd.settrace (didn't perform)") + #_LOGGER.info("Exception: {0}".format(e)) + #pass + #else: + #pass +## ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# validate access to the DCCsi and it's Lib site-packages +# bootstrap site-packages by version +from azpy.constants import PATH_DCCSI_PYTHON_LIB_PATH + +try: + os.path.exists(PATH_DCCSI_PYTHON_LIB_PATH) + site.addsitedir(PATH_DCCSI_PYTHON_LIB_PATH) + _LOGGER.info('azpy 3rdPary site-packages: is: {0}'.format(PATH_DCCSI_PYTHON_LIB_PATH)) +except Exception as e: + _LOGGER.error('ERROR: {0}, {1}'.format(e, PATH_DCCSI_PYTHON_LIB_PATH)) + raise e + +# 3rdparty +from unipath import Path +from box import Box +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +# Maya is frozen +#_MODULE_PATH = Path(__file__) +# https://tinyurl.com/y49t3zzn +# module path when frozen +_MODULE_FILEPATH = os.path.abspath(inspect.getfile(inspect.currentframe())) +_MODULE_PATH = os.path.dirname(_MODULE_FILEPATH) +if _BOOT_INFO: + _LOGGER.debug('Boot: CWD: {}'.format(os.getcwd())) + _LOGGER.debug('Frozen: _MODULE_FILEPATH: {}'.format(_MODULE_FILEPATH)) + _LOGGER.debug('Frozen: _MODULE_PATH: {}'.format(_MODULE_PATH)) + _LOGGER.debug('Module __name__: {}'.format(__name__)) +# root: INFO: Module __name__: __main__ + +_LOGGER.info('_MODULENAME: {}'.format(_MODULENAME)) + +# ------------------------------------------------------------------------- +# check some env var tags (fail if no, likely means no proper code access) +_STR_ERROR_ENVAR = "Envar 'key' does not exist in base_env: {0}" +_DCCSI_TOOLS_PATH = None +# To Do: needs to be updated to use dynaconf and config.py +try: + _DCCSI_TOOLS_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSI_TOOLS_PATH] +except Exception as e: + _LOGGER.critical(_STR_ERROR_ENVAR.format(_BASE_ENVVAR_DICT[ENVAR_DCCSI_TOOLS_PATH])) + +_O3DE_PROJECT_PATH = None +try: + _O3DE_PROJECT_PATH = _BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT_PATH] +except Exception as e: + _LOGGER.critical(_STR_ERROR_ENVAR.format(_BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT_PATH])) + +# check some env var tags (fail if no, likely means no proper code access) +_O3DE_DEV = _BASE_ENVVAR_DICT[ENVAR_O3DE_DEV] +_O3DE_DCCSIG_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSIG_PATH] +_O3DE_DCCSI_LOG_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSI_LOG_PATH] +_O3DE_AZPY_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSI_AZPY_PATH] +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# To Do: implement data driven config +# Currently not used, but will be where we store the ordered dict +# which is parsed from the project bootstrapping config files. +_G_app_config = {} + +# global scope maya callbacks container +_G_callbacks = Box(box_dots=True) # global scope container + +# used to store fixPaths in the global scope +_fix_paths = None +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# add appropriate common tools paths to the maya environment variables +def startup(): + """Early starup execution before mayautils.executeDeferred(). + Some things like UI and plugins should be defered to avoid failure""" + _LOGGER.info('startup() fired') + + # get known paths + _KNOWN_PATHS = site._init_pathinfo() + + if os.path.isdir(_DCCSI_TOOLS_PATH): + site.addsitedir(_DCCSI_TOOLS_PATH, _KNOWN_PATHS) + try: + import azpy.test + _LOGGER.info('SUCCESS, import azpy.test') + except Exception as e: + _LOGGER.warning('startup(), could not import azpy.test') + + _LOGGER.info('startup(), COMPLETE') + return 0 +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# verify Shared\Python exists and add it as a site dir. Begin imports and config. +def post_startup(): + """Allows for a defered execution startup sequence""" + + _LOGGER.info('post_startup() fired') + + # plugins, To Do: these should be moved to bootstrapping config + try: + maya.cmds.loadPlugin("dx11Shader") + except Exception as e: + _LOGGER.error(e) # not a hard failure + + # Lumberyard DCCsi environment ready or error out. + try: + import azpy.dcc.maya + _LOGGER.info('Python module imported: azpy.dcc.maya') + except Exception as e: + _LOGGER.error(e) + _LOGGER.error(traceback.print_exc()) + return 1 + + # Dccsi azpy maya ready or error out. + try: + azpy.dcc.maya.init() + _LOGGER.info('SUCCESS, azpy.dcc.maya.init(), code accessible.') + except Exception as e: + _LOGGER.error(e) + _LOGGER.error(traceback.print_exc()) + return 1 + + # callbacks, To Do: these should also be moved to the bootstrapping config + # Defered startup after the Ui is running. + _G_CALLBACKS = Box(box_dots=True) # this just ensures a global scope container + if _G_LOAD_CALLBACKS: + from set_callbacks import _G_CALLBACKS + # ^ need to hold on to this as the install repopulate set + + # this ensures the fixPaths callback is loaded + # even when the other global callbacks are disabled + from set_callbacks import install_fix_paths + install_fix_paths() + + # set the project workspace + #_O3DE_PROJECT_PATH = _BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT_PATH] + _project_workspace = os.path.join(_O3DE_PROJECT_PATH, TAG_MAYA_WORKSPACE) + if os.path.isfile(_project_workspace): + try: + # load workspace + maya.cmds.workspace(_O3DE_PROJECT_PATH, openWorkspace=True) + _LOGGER.info('Loaded workspace file: {0}'.format(_project_workspace)) + maya.cmds.workspace(_O3DE_PROJECT_PATH, update=True) + except Exception as e: + _LOGGER.error(e) + else: + _LOGGER.warning('Workspace file not found: {1}'.format(_O3DE_PROJECT_PATH)) + + # Set up Lumberyard, maya default setting + from set_defaults import set_defaults + set_defaults() + + # Setup UI tools + if not maya.cmds.about(batch=True): + _LOGGER.info('Add UI dependent tools') + # wrap in a try, because we haven't implmented it yet + try: + mel.eval(str(r'source "{}"'.format(TAG_O3DE_DCC_MAYA_MEL))) + except Exception as e: + _LOGGER.error(e) + + # manage custom menu in a sub-module + from set_menu import set_main_menu + set_main_menu() + + # To Do: manage custom shelf in a sub-module + + _LOGGER.info('post_startup(), COMPLETE') + _LOGGER.info('DCCsi Bootstrap, COMPLETE') + return 0 +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +if __name__ == '__main__': + try: + # Early startup config. + startup() + + # This allows defered action post boot (atfer UI is active) + from maya.utils import executeDeferred + post = executeDeferred(post_startup) + + except Exception as e: + traceback.print_exc() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Shaders/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Shaders/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Tools/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Tools/stub new file mode 100644 index 0000000000..d365a5f2c9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Tools/stub @@ -0,0 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/config.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/config.py new file mode 100644 index 0000000000..50d6de8de4 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/config.py @@ -0,0 +1,23 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- +# DCCsi\\Tools\\DCC\\Maya\\config.py + +"""DccScriptingInterface (DCCsi) +This is the dynamic config (dynaconf) for O3DE Maya DCCsi interface +""" + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + """Run this file as main""" + + print('Maya.config() not implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/constants.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/constants.py new file mode 100644 index 0000000000..6695b6185c --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/constants.py @@ -0,0 +1,16 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- +# DCCsi\\Tools\\DCC\\Maya\\constsants.py + +"""DccScriptingInterface (DCCsi) +This module contains constants for the O3DE Maya DCCsi interface +""" + diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/readme.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/readme.txt new file mode 100644 index 0000000000..57c8317737 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/readme.txt @@ -0,0 +1,78 @@ +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 +------------------------------------------------------------------------------- + +"DccScriptingInterface" aka DCCsi is a Gem for O3DE to extend and interface with dcc tools +in the python ecosystem. Each dcc tool may have it's own specific version of python. +Most are some version of py3+. O3DE provides an install of py3+ and manages package +dependancies with requirements.txt files and the cmake build system. + +However Autodesk Maya still uses a version of py2.7 and so we need an alternate way +to deal with package management for this DCC tool. + +Maya ships with it's own python interpreter called mayapy.exe + +Generally it is located here: +C:\Program Files\Autodesk\Maya2020\bin\mayapy.exe + +The python install and site-packages are here: +C:\Program Files\Autodesk\Maya2020\Python\Lib\site-packages + +A general goal of the DCCsi is be self-maintained, and to not taint the users installed applications of environment. + +So we boostrap additional access to site-packages in our userSetup.py: +"C:\Depot\Lumberyard\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\SDK\Maya\Scripts\userSetup.py" + +We don't want users to have to install or use Python2.7 although with maya and possibly other dcc tools we don't have that control. Maya 2020 and earlier versions are still on Python2.7, so instead of forcing another install of python we can just use mayapy to manage extensions. + +Pip may already be installed, you can check like so (your maya install path may be different): + +C:\Program Files\Autodesk\Maya2020\bin>mayapy -m pip --version + +If pip is not available yet for your mayapy. + +First find out where th site-packages is located + +C:\Program Files\Autodesk\Maya2020\bin>mayapy -m site +sys.path = [ + 'C:\\Program Files\\Autodesk\\Maya2020\\bin', + 'C:\\Program Files\\Autodesk\\Maya2020\\bin\\python27.zip', + 'C:\\Program Files\\Autodesk\\Maya2020\\Python\\DLLs', + 'C:\\Program Files\\Autodesk\\Maya2020\\Python\\lib', + 'C:\\Program Files\\Autodesk\\Maya2020\\Python\\lib\\plat-win', + 'C:\\Program Files\\Autodesk\\Maya2020\\Python\\lib\\lib-tk', + 'C:\\Program Files\\Autodesk\\Maya2020\\Python', + 'C:\\Program Files\\Autodesk\\Maya2020\\Python\\lib\\site-packages', +] +USER_BASE: 'C:\\Users\\gallowj\\AppData\\Roaming\\Python' (exists) +USER_SITE: 'C:\\Users\\gallowj\\AppData\\Roaming\\Python\\Python27\\site-packages' (doesn't exist) +ENABLE_USER_SITE: True + +This is the location we are looking for: +C:\\Program Files\\Autodesk\\Maya2020\\Python\\lib\\site-packages + +download get-pip.py and put into the above ^ directory: +https://bootstrap.pypa.io/pip/2.7/get-pip.py + +Put that in the root of site-packages: +C:\\Program Files\\Autodesk\\Maya2020\\Python\\lib\\site-packages\\get-pip.py + +With get-pip module ready, we run it to install pip: +C:\Program Files\Autodesk\Maya2020\bin>mayapy -m get-pip + +Now you should be able to run the following command and verify pip: +C:\Program Files\Autodesk\Maya2020\bin>mayapy -m pip --version +pip 20.3.4 from C:\Users\< you >\AppData\Roaming\Python\Python27\site-packages\pip (python 2.7) + +Now your local maya install is all set up with pip so you can install additional python packages to use in maya. (note: not all packages are compatible with maya) + +Now you will want to run the following file to finish setup... +We have a requirements.txt file with the extension packages we use in the DCCsi. +You'll need the repo/branch path of your O3DE (aka Lumberyard) install. +And you'll need to know where the DCCsi is located, we will install package dependancies there. + +Note: you may need to update the paths below to match your local o3de engine install! + +C:\Program Files\Autodesk\Maya2020\bin>mayapy -m pip install -r C:\Depot\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\SDK\Maya\requirements.txt -t C:\Depot\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\3rdParty\Python\Lib\2.x\2.7.x\site-packages diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/requirements.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/requirements.txt new file mode 100644 index 0000000000..ceb5be4dea --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/requirements.txt @@ -0,0 +1,84 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --generate-hashes requirements.txt +# +certifi==2020.6.20 \ + --hash=sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3 \ + --hash=sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41 + # via -r requirements.txt +cachetools==3.1.1 \ + --hash=sha256:428266a1c0d36dc5aca63a2d7c5942e88c2c898d72139fca0e97fdd2380517ae \ + --hash=sha256:8ea2d3ce97850f31e4a08b0e2b5e6c34997d7216a9d2c98e0f3978630d4da69a + # via -r requirements.txt +click==7.1.2 \ + --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a \ + --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc + # via + # -r requirements.txt + # pip-tools +dynaconf==3.1.4 \ + --hash=sha256:b2f472d83052f809c5925565b8a2ba76a103d5dc1dbb9748b693ed67212781b9 \ + --hash=sha256:e6f383b84150b70fc439c8b2757581a38a58d07962aa14517292dcce1a77e160 + # via -r requirements.txt +hashids==1.3.1 \ + --hash=sha256:6c3dc775e65efc2ce2c157a65acb776d634cb814598f406469abef00ae3f635c \ + --hash=sha256:8bddd1acba501bfc9306e7e5a99a1667f4f2cacdc20cbd70bcc5ddfa5147c94c + # via -r requirements.txt +pathlib2==2.3.5 \ + --hash=sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db \ + --hash=sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868 + # via -r requirements.txt +pathlib==1.0.1 \ + --hash=sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f + # via -r requirements.txt +python-box==3.4.6 \ + --hash=sha256:694a7555e3ff9fbbce734bbaef3aad92b8e4ed0659d3ed04d56b6a0a0eff26a9 \ + --hash=sha256:a71d3dc9dbaa34c8597d3517c89a8041bd62fa875f23c0f3dad55e1958e3ce10 + # via -r requirements.txt +scandir==1.10.0 \ + --hash=sha256:2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e \ + --hash=sha256:2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022 \ + --hash=sha256:2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f \ + --hash=sha256:2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f \ + --hash=sha256:4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae \ + --hash=sha256:67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173 \ + --hash=sha256:7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4 \ + --hash=sha256:8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32 \ + --hash=sha256:92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188 \ + --hash=sha256:b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d \ + --hash=sha256:cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac + # via + # -r requirements.txt + # pathlib2 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced + # via + # -r requirements.txt + # pathlib2 +typing==3.7.4.3 \ + --hash=sha256:1187fb9c82fd670d10aa07bbb6cfcfe4bdda42d6fab8d5134f04e8c4d0b71cc9 \ + --hash=sha256:283d868f5071ab9ad873e5e52268d611e851c870a2ba354193026f2dfb29d8b5 + # via + # -r requirements.txt + # dynaconf +unipath==1.1 \ + --hash=sha256:09839adcc72e8a24d4f76d63656f30b5a1f721fc40c9bcd79d8c67bdd8b47dae \ + --hash=sha256:e6257e508d8abbfb6ddd8ec357e33589f1f48b1599127f23b017124d90b0fff7 + # via -r requirements.txt +qdarkstyle==3.0.2 \ + --hash=sha256:55d149cf5f40ee297397f1818e091118cefb855a4a9c5c38566c47acd2d8c7ae \ + --hash=sha256:7c791535cc20b3cc1e8e1bf6b88dabe53cb0615983df702be83597e73ada2558 + # via -r c:\temp\requirements.txt +qtpy==1.9.0 \ + --hash=sha256:2db72c44b55d0fe1407be8fba35c838ad0d6d3bb81f23007886dc1fc0f459c8d \ + --hash=sha256:fa0b8363b363e89b2a6f49eddc162a04c0699ae95e109a6be3bb145a913190ea + # via + # -r c:\temp\requirements.txt + # qdarkstyle +wincertstore==0.2 \ + --hash=sha256:22d5eebb52df88a8d4014d5cf6d1b6c3a5d469e6c3b2e2854f3a003e48872356 \ + --hash=sha256:780bd1557c9185c15d9f4221ea7f905cb20b93f7151ca8ccaed9714dce4b327a + # via -r requirements.txt diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/settings.json b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/settings.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/start.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/start.py new file mode 100644 index 0000000000..4d965aec28 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/start.py @@ -0,0 +1,24 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- +# DCCsi\\Tools\\DCC\\Maya\\start.py + +"""DccScriptingInterface (DCCsi) +The DCCsi bootstraps tools like Maya with additional code access and extensions. +This module starts up maya in the DCCsi managed synthetic environment context. +""" + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + """Run this file as main""" + + print('Maya.Start() not implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/custom_shaders/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/custom_shaders/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/ly_presets/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/ly_presets/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/templates/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/templates/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/scripts/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/scripts/stub new file mode 100644 index 0000000000..d365a5f2c9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/scripts/stub @@ -0,0 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/stub new file mode 100644 index 0000000000..d365a5f2c9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/stub @@ -0,0 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/.gitignore b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/.gitignore similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/.gitignore rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/.gitignore diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Core.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Core.bat similarity index 67% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Core.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Core.bat index 5146d5f3e8..b37170d605 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Core.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Core.bat @@ -31,11 +31,11 @@ echo ~ O3DE DCC Scripting Interface Environment ... echo _____________________________________________________________________ echo. -IF "%DCCSI_LAUNCHERS_PATH%"=="" (set DCCSI_LAUNCHERS_PATH=%~dp0) -echo DCCSI_LAUNCHERS_PATH = %DCCSI_LAUNCHERS_PATH% +IF "%DCCSI_DEV_ENV%"=="" (set DCCSI_DEV_ENV=%~dp0) +echo DCCSI_DEV_ENV = %DCCSI_DEV_ENV% :: add to the PATH -SET PATH=%DCCSI_LAUNCHERS_PATH%;%PATH% +SET PATH=%DCCSI_DEV_ENV%;%PATH% :: Constant Vars (Global) :: global debug flag (propogates) @@ -61,24 +61,21 @@ IF "%DCCSI_LOGLEVEL%"=="" (set DCCSI_LOGLEVEL=20) echo DCCSI_LOGLEVEL = %DCCSI_LOGLEVEL% :: This maps up to the \Dev folder -IF "%DEV_REL_PATH%"=="" (set DEV_REL_PATH=..\..\..\..) -echo DEV_REL_PATH = %DEV_REL_PATH% +IF "%O3DE_REL_PATH%"=="" (set O3DE_REL_PATH=..\..\..\..) +echo O3DE_REL_PATH = %O3DE_REL_PATH% :: You can define the project name -IF "%LY_PROJECT_NAME%"=="" ( - for %%a in (%CD%..\..\..) do set LY_PROJECT_NAME=%%~na +IF "%O3DE_PROJECT%"=="" ( + for %%a in (%CD%..\..\..\..) do set O3DE_PROJECT=%%~na ) -echo LY_PROJECT_NAME = %LY_PROJECT_NAME% - -:: if not defined we just use the DCCsi path as stand-in -IF "%LY_PROJECT%"=="" (set LY_PROJECT=%CD%) -echo LY_PROJECT = %LY_PROJECT% +echo O3DE_PROJECT = %O3DE_PROJECT% :: set up the default project path (dccsi) :: if not set we also use the DCCsi path as stand-in -CD /D ..\..\ -IF "%LY_PROJECT_PATH%"=="" (set LY_PROJECT_PATH=%CD%) -echo LY_PROJECT_PATH = %LY_PROJECT_PATH% +CD /D ..\..\..\ +:: To Do: remove one of these +IF "%O3DE_PROJECT_PATH%"=="" (set O3DE_PROJECT_PATH=%CD%) +echo O3DE_PROJECT_PATH = %O3DE_PROJECT_PATH% IF "%ABS_PATH%"=="" (set ABS_PATH=%CD%) echo ABS_PATH = %ABS_PATH% @@ -87,37 +84,40 @@ echo ABS_PATH = %ABS_PATH% pushd %ABS_PATH% :: Change to root Lumberyard dev dir -CD /d %LY_PROJECT_PATH%\%DEV_REL_PATH% -IF "%LY_DEV%"=="" (set LY_DEV=%CD%) -echo LY_DEV = %LY_DEV% +CD /d %O3DE_PROJECT_PATH%\%O3DE_REL_PATH% +IF "%O3DE_DEV%"=="" (set O3DE_DEV=%CD%) +echo O3DE_DEV = %O3DE_DEV% :: Restore original directory popd :: dcc scripting interface gem path :: currently know relative path to this gem -set DCCSIG_PATH=%LY_DEV%\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface +set DCCSIG_PATH=%O3DE_DEV%\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface echo DCCSIG_PATH = %DCCSIG_PATH% :: Change to DCCsi root dir CD /D %DCCSIG_PATH% :: per-dcc sdk path -set DCCSI_SDK_PATH=%DCCSIG_PATH%\SDK -echo DCCSI_SDK_PATH = %DCCSI_SDK_PATH% +set DCCSI_TOOLS_PATH=%DCCSIG_PATH%\Tools +echo DCCSI_TOOLS_PATH = %DCCSI_TOOLS_PATH% :: temp log location specific to this gem -set DCCSI_LOG_PATH=%DCCSIG_PATH%\.temp\logs +set DCCSI_LOG_PATH=%O3DE_PROJECT_PATH%\.temp\logs echo DCCSI_LOG_PATH = %DCCSI_LOG_PATH% :: O3DE build path -IF "%TAG_LY_BUILD_PATH%"=="" (set TAG_LY_BUILD_PATH=build) -echo TAG_LY_BUILD_PATH = %TAG_LY_BUILD_PATH% +IF "%O3DE_BUILD_FOLDER%"=="" (set O3DE_BUILD_FOLDER=build) +echo O3DE_BUILD_FOLDER = %O3DE_BUILD_FOLDER% + +IF "%O3DE_BUILD_PATH%"=="" (set O3DE_BUILD_PATH=%O3DE_DEV%\%O3DE_BUILD_FOLDER%) +echo O3DE_BUILD_PATH = %O3DE_BUILD_PATH% -IF "%LY_BUILD_PATH%"=="" (set LY_BUILD_PATH=%LY_DEV%\%TAG_LY_BUILD_PATH%\bin\profile) -echo LY_BUILD_PATH = %LY_BUILD_PATH% +IF "%O3DE_BIN_PATH%"=="" (set O3DE_BIN_PATH=%O3DE_BUILD_PATH%\bin\profile) +echo O3DE_BIN_PATH = %O3DE_BIN_PATH% :: add to the PATH -SET PATH=%LY_BUILD_PATH%;%DCCSIG_PATH%;%DCCSI_AZPY_PATH%;%PATH% +SET PATH=%O3DE_BIN_PATH%;%DCCSIG_PATH%;%PATH% ::ENDLOCAL diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Maya.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Maya.bat similarity index 87% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Maya.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Maya.bat index cceaa80b75..5e3c600124 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Maya.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Maya.bat @@ -27,14 +27,12 @@ IF "%DCCSI_PY_VERSION_MINOR%"=="" (set DCCSI_PY_VERSION_MINOR=7) IF "%DCCSI_PY_VERSION_RELEASE%"=="" (set DCCSI_PY_VERSION_RELEASE=11) :: Default Maya Version -IF "%DCCSI_MAYA_VERSION%"=="" (set DCCSI_MAYA_VERSION=%MAYA_VERSION%) +IF "%DCCSI_MAYA_VERSION%"=="" (set DCCSI_MAYA_VERSION=2020) :: Initialize env CALL %~dp0\Env_Core.bat CALL %~dp0\Env_Python.bat -::SETLOCAL ENABLEDELAYEDEXPANSION - echo. echo _____________________________________________________________________ echo. @@ -48,14 +46,14 @@ echo DCCSI_PY_VERSION_RELEASE = %DCCSI_PY_VERSION_RELEASE% echo DCCSI_MAYA_VERSION = %DCCSI_MAYA_VERSION% :::: Set Maya native project acess to this project -IF "%MAYA_PROJECT%"=="" (set MAYA_PROJECT=%LY_PROJECT%) +IF "%MAYA_PROJECT%"=="" (set MAYA_PROJECT=%O3DE_PROJECT%) echo MAYA_PROJECT = %MAYA_PROJECT% :: maya sdk path -set DCCSI_SDK_MAYA_PATH=%DCCSI_SDK_PATH%\Maya -echo DCCSI_SDK_MAYA_PATH = %DCCSI_SDK_MAYA_PATH% +set DCCSI_TOOLS_MAYA_PATH=%DCCSI_TOOLS_PATH%\DCC\Maya +echo DCCSI_TOOLS_MAYA_PATH = %DCCSI_TOOLS_MAYA_PATH% -set MAYA_MODULE_PATH=%DCCSI_SDK_MAYA_PATH%;%MAYA_MODULE_PATH% +set MAYA_MODULE_PATH=%DCCSI_TOOLS_MAYA_PATH%;%MAYA_MODULE_PATH% echo MAYA_MODULE_PATH = %MAYA_MODULE_PATH% :: Maya File Paths, etc @@ -93,36 +91,36 @@ echo DCCSI_PY_MAYA = %DCCSI_PY_MAYA% SET PATH=%MAYA_BIN_PATH%;%PATH% :: Local DCCsi Maya plugins access (ours) -set DCCSI_MAYA_PLUG_IN_PATH=%DCCSI_SDK_MAYA_PATH%\plugins +set DCCSI_MAYA_PLUG_IN_PATH=%DCCSI_TOOLS_MAYA_PATH%\plugins :: also attached to maya's built-it env var set MAYA_PLUG_IN_PATH=%DCCSI_MAYA_PLUG_IN_PATH%;MAYA_PLUG_IN_PATH echo DCCSI_MAYA_PLUG_IN_PATH = %DCCSI_MAYA_PLUG_IN_PATH% :: Local DCCsi Maya shelves (ours) -set DCCSI_MAYA_SHELF_PATH=%DCCSI_SDK_MAYA_PATH%\Prefs\Shelves +set DCCSI_MAYA_SHELF_PATH=%DCCSI_TOOLS_MAYA_PATH%\Prefs\Shelves set MAYA_SHELF_PATH=%DCCSI_MAYA_SHELF_PATH% echo DCCSI_MAYA_SHELF_PATH = %DCCSI_MAYA_SHELF_PATH% :: Local DCCsi Maya icons path (ours) -set DCCSI_MAYA_XBMLANGPATH=%DCCSI_SDK_MAYA_PATH%\Prefs\icons +set DCCSI_MAYA_XBMLANGPATH=%DCCSI_TOOLS_MAYA_PATH%\Prefs\icons :: also attached to maya's built-it env var set XBMLANGPATH=%DCCSI_MAYA_XBMLANGPATH%;%XBMLANGPATH% echo DCCSI_MAYA_XBMLANGPATH = %DCCSI_MAYA_XBMLANGPATH% :: Local DCCsi Maya Mel scripts (ours) -set DCCSI_MAYA_SCRIPT_MEL_PATH=%DCCSI_SDK_MAYA_PATH%\Scripts\Mel +set DCCSI_MAYA_SCRIPT_MEL_PATH=%DCCSI_TOOLS_MAYA_PATH%\Scripts\Mel :: also attached to maya's built-it env var set MAYA_SCRIPT_PATH=%DCCSI_MAYA_SCRIPT_MEL_PATH%;%MAYA_SCRIPT_PATH% echo DCCSI_MAYA_SCRIPT_MEL_PATH = %DCCSI_MAYA_SCRIPT_MEL_PATH% :: Local DCCsi Maya Python scripts (ours) -set DCCSI_MAYA_SCRIPT_PY_PATH=%DCCSI_SDK_MAYA_PATH%\Scripts\Python +set DCCSI_MAYA_SCRIPT_PY_PATH=%DCCSI_TOOLS_MAYA_PATH%\Scripts\Python :: also attached to maya's built-it env var set MAYA_SCRIPT_PATH=%DCCSI_MAYA_SCRIPT_PY_PATH%;%MAYA_SCRIPT_PATH% echo DCCSI_MAYA_SCRIPT_PY_PATH = %DCCSI_MAYA_SCRIPT_PY_PATH% :: DCCsi Maya boostrap, userSetup.py access (ours) -set DCCSI_MAYA_SCRIPT_PATH=%DCCSI_SDK_MAYA_PATH%\Scripts +set DCCSI_MAYA_SCRIPT_PATH=%DCCSI_TOOLS_MAYA_PATH%\Scripts :: also attached to maya's built-it env var set MAYA_SCRIPT_PATH=%DCCSI_MAYA_SCRIPT_PATH%;%MAYA_SCRIPT_PATH% echo DCCSI_MAYA_SCRIPT_PATH = %DCCSI_MAYA_SCRIPT_PATH% diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_PyCharm.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_PyCharm.bat similarity index 95% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_PyCharm.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_PyCharm.bat index 67f2c3d81e..7be9154e6d 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_PyCharm.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_PyCharm.bat @@ -34,7 +34,7 @@ CALL %~dp0\Env_Python.bat CALL %~dp0\Env_Qt.bat :: Wing and other IDEs probably prefer access directly to the python.exe -set DCCSI_PY_IDE = %DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python +set DCCSI_PY_IDE = %O3DE_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python echo DCCSI_PY_IDE = %DCCSI_PY_IDE% :: ide and debugger plug diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Python.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Python.bat similarity index 76% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Python.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Python.bat index 196d7b09bb..388ab531df 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Python.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Python.bat @@ -19,8 +19,6 @@ PUSHD %~dp0 CALL %~dp0\Env_Core.bat -::SETLOCAL ENABLEDELAYEDEXPANSION - echo. echo _____________________________________________________________________ echo. @@ -56,32 +54,29 @@ echo DCCSI_PYTHON_LIB_PATH = %DCCSI_PYTHON_LIB_PATH% SET PATH=%DCCSI_PYTHON_LIB_PATH%;%PATH% :: shared location for default O3DE python location -set DCCSI_PYTHON_INSTALL=%LY_DEV%\Python -echo DCCSI_PYTHON_INSTALL = %DCCSI_PYTHON_INSTALL% +set O3DE_PYTHON_INSTALL=%O3DE_DEV%\python +echo O3DE_PYTHON_INSTALL = %O3DE_PYTHON_INSTALL% :: location for O3DE python 3.7 location -set DCCSI_PY_BASE=%DCCSI_PYTHON_INSTALL%\python.cmd +:: Note, many DCC tools (like Maya) include thier own python interpretter +:: Some DCC apps may not operate correctly if PYTHONHOME is set (this is definitely the case with Maya) +:: Be aware the python.cmd below does set PYTHONHOME +set DCCSI_PY_BASE=%O3DE_PYTHON_INSTALL%\python.cmd echo DCCSI_PY_BASE = %DCCSI_PY_BASE% -:: ide and debugger plug -set DCCSI_PY_DEFAULT=%DCCSI_PY_BASE% +CALL %O3DE_PYTHON_INSTALL%\get_python_path.bat -:: Wing and other IDEs probably prefer access directly to the python.exe -set DCCSI_PY_IDE=%DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python +:: Some IDEs like Wing, may in some cases need acess directly to the exe to operate correctly +IF "%DCCSI_PY_IDE%"=="" (set DCCSI_PY_IDE=%O3DE_PYTHONHOME%\python.exe) echo DCCSI_PY_IDE = %DCCSI_PY_IDE% -set DCCSI_PY_IDE_PACKAGES=%DCCSI_PY_IDE%\Lib\site-packages -echo DCCSI_PY_IDE_PACKAGES = %DCCSI_PY_IDE_PACKAGES% - :: add to the PATH -SET PATH=%DCCSI_PYTHON_INSTALL%;%DCCSI_PY_IDE%;%DCCSI_PY_IDE_PACKAGES%;%PATH% +SET PATH=%O3DE_PYTHON_INSTALL%;%O3DE_PYTHONHOME%;%DCCSI_PY_IDE%;%PATH% :: add all python related paths to PYTHONPATH for package imports -set PYTHONPATH=%DCCSIG_PATH%;%DCCSI_PYTHON_LIB_PATH%;%LY_BUILD_PATH%;%PYTHONPATH% +set PYTHONPATH=%DCCSIG_PATH%;%DCCSI_PYTHON_LIB_PATH%;%O3DE_BUILD_PATH%;%PYTHONPATH% echo PYTHONPATH = %PYTHONPATH% -::ENDLOCAL - :: Set flag so we don't initialize dccsi environment twice SET DCCSI_ENV_PY_INIT=1 GOTO END_OF_FILE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Qt.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Qt.bat similarity index 85% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Qt.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Qt.bat index 2ad49b4416..6202ead2f2 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Qt.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Qt.bat @@ -34,23 +34,23 @@ echo. :: set up Qt/Pyside paths :: set up PySide2/Shiboken -set QTFORPYTHON_PATH=%LY_DEV%\Gems\QtForPython\3rdParty\pyside2\windows\release +set QTFORPYTHON_PATH=%O3DE_DEV%\Gems\QtForPython\3rdParty\pyside2\windows\release echo QTFORPYTHON_PATH = %QTFORPYTHON_PATH% :: add to the PATH SET PATH=%QTFORPYTHON_PATH%;%PATH% SET PYTHONPATH=%QTFORPYTHON_PATH%;%PYTHONPATH% -set QT_PLUGIN_PATH=%LY_BUILD_PATH%\bin\profile\EditorPlugins +set QT_PLUGIN_PATH=%O3DE_BUILD_PATH%\bin\profile\EditorPlugins echo QT_PLUGIN_PATH = %QT_PLUGIN_PATH% :: add to the PATH SET PATH=%QT_PLUGIN_PATH%;%PATH% SET PYTHONPATH=%QT_PLUGIN_PATH%;%PYTHONPATH% -set LY_BIN_PATH=%LY_BUILD_PATH%\bin\profile -echo LY_BIN_PATH = %LY_BIN_PATH% -SET PATH=%LY_BIN_PATH%;%PATH% +set O3DE_BIN_PATH=%O3DE_BUILD_PATH%\bin\profile +echo O3DE_BIN_PATH = %O3DE_BIN_PATH% +SET PATH=%O3DE_BIN_PATH%;%PATH% ::ENDLOCAL diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Substance.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Substance.bat similarity index 92% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Substance.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Substance.bat index 3c92b306ca..d44ed985da 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Substance.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Substance.bat @@ -31,7 +31,7 @@ echo. : Substance Designer :: maya sdk path -set DCCSI_SUBSTANCE_PATH=%DCCSI_SDK_PATH%\Substance +set DCCSI_SUBSTANCE_PATH=%DCCSI_TOOLS_PATH%\Substance echo DCCSI_SUBSTANCE_PATH = %DCCSI_SUBSTANCE_PATH% :: https://docs.substance3d.com/sddoc/project-preferences-107118596.html#ProjectPreferences-ConfigurationFile :: Path to .exe, "C:\Program Files\Allegorithmic\Substance Designer\Substance Designer.exe" @@ -39,7 +39,7 @@ set SUBSTANCE_PATH="%ProgramFiles%\Allegorithmic\Substance Designer" echo SUBSTANCE_PATH = %SUBSTANCE_PATH% :: default config -set SUBSTANCE_CFG_PATH=%LY_PROJECT_PATH%\DCCsi_default.sbscfg +set SUBSTANCE_CFG_PATH=%O3DE_PROJECT_PATH%\DCCsi_default.sbscfg echo SUBSTANCE_CFG_PATH = %SUBSTANCE_CFG_PATH% ::ENDLOCAL diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_VScode.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_VScode.bat similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_VScode.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_VScode.bat diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_WingIDE.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_WingIDE.bat similarity index 78% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_WingIDE.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_WingIDE.bat index 56209ca5aa..a8c3b3d073 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_WingIDE.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_WingIDE.bat @@ -27,18 +27,9 @@ CALL %~dp0\Env_Core.bat CALL %~dp0\Env_Python.bat CALL %~dp0\Env_Qt.bat -:: Wing and other IDEs probably prefer access directly to the python.exe -set DCCSI_PY_IDE = %DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python -echo DCCSI_PY_IDE = %DCCSI_PY_IDE% - -:: ide and debugger plug -set DCCSI_PY_DEFAULT=%DCCSI_PY_IDE%\python.exe - :: put project env variables/paths here set WINGHOME=%PROGRAMFILES(X86)%\Wing Pro %DCCSI_WING_VERSION_MAJOR%.%DCCSI_WING_VERSION_MINOR% -SET WING_PROJ=%DCCSIG_PATH%\Solutions\.wing\DCCsi_%DCCSI_WING_VERSION_MAJOR%x.wpr - -::SETLOCAL ENABLEDELAYEDEXPANSION +SET WING_PROJ=%DCCSIG_PATH%\Tools\Dev\Windows\Solutions\.wing\DCCsi_%DCCSI_WING_VERSION_MAJOR%x.wpr echo. echo _____________________________________________________________________ @@ -55,8 +46,6 @@ echo WING_PROJ = %WING_PROJ% :: add to the PATH SET PATH=%WINGHOME%;%PATH% -::ENDLOCAL - :: Set flag so we don't initialize dccsi environment twice SET DCCSI_ENV_WINGIDE_INIT=1 GOTO END_OF_FILE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Env_Cmd.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Env_Cmd.bat similarity index 97% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Env_Cmd.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Env_Cmd.bat index a8476ccc22..5b76de1401 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Env_Cmd.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Env_Cmd.bat @@ -40,7 +40,7 @@ CALL %~dp0\Env_WingIDE.bat IF EXIST "%~dp0Env_Dev.bat" CALL %~dp0Env_Dev.bat :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% :: Create command prompt with environment CALL %windir%\system32\cmd.exe diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_MayaPy_PyCharmPro.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_MayaPy_PyCharmPro.bat similarity index 91% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_MayaPy_PyCharmPro.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_MayaPy_PyCharmPro.bat index d98b8d81e7..e451050ab8 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_MayaPy_PyCharmPro.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_MayaPy_PyCharmPro.bat @@ -56,14 +56,14 @@ echo ~ MayaPy.exe (default python interpreter) echo _____________________________________________________________________ echo. -echo LY_DEV = %LY_DEV% +echo O3DE_DEV = %O3DE_DEV% :: shared location for default O3DE python location -set DCCSI_PYTHON_INSTALL=%LY_DEV%\Python -echo DCCSI_PYTHON_INSTALL = %DCCSI_PYTHON_INSTALL% +set O3DE_PYTHON_INSTALL=%O3DE_DEV%\Python +echo O3DE_PYTHON_INSTALL = %O3DE_PYTHON_INSTALL% :: Wing and other IDEs probably prefer access directly to the python.exe -set DCCSI_PY_IDE = %DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python +set DCCSI_PY_IDE = %O3DE_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python echo DCCSI_PY_IDE = %DCCSI_PY_IDE% :: ide and debugger plug @@ -80,7 +80,7 @@ IF EXIST "%~dp0Env_Dev.bat" CALL %~dp0Env_Dev.bat echo. :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% IF EXIST "%PYCHARM_HOME%\bin\pycharm64.exe" ( start "" "%PYCHARM_HOME%\bin\pycharm64.exe" "%PYCHARM_PROJ%" diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Maya_2020.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Maya_2020.bat similarity index 98% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Maya_2020.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Maya_2020.bat index 2c4e1cc941..ee2ea5bf1f 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Maya_2020.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Maya_2020.bat @@ -50,7 +50,7 @@ echo MAYA_LOCATION = %MAYA_LOCATION% echo MAYA_BIN_PATH = %MAYA_BIN_PATH% :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% :: Default to the right version of Maya if we can detect it... and launch IF EXIST "%MAYA_BIN_PATH%\maya.exe" ( diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_PyCharmPro.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_PyCharmPro.bat similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_PyCharmPro.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_PyCharmPro.bat diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_PyMin_Cmd.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_PyMin_Cmd.bat similarity index 97% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_PyMin_Cmd.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_PyMin_Cmd.bat index d001d8a75e..193cb40de8 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_PyMin_Cmd.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_PyMin_Cmd.bat @@ -38,7 +38,7 @@ echo _____________________________________________________________________ echo. :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% :: Create command prompt with environment CALL %windir%\system32\cmd.exe diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Qt_PyMin_Cmd.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Qt_PyMin_Cmd.bat similarity index 93% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Qt_PyMin_Cmd.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Qt_PyMin_Cmd.bat index d50989004b..75d1ff8cc4 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Qt_PyMin_Cmd.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Qt_PyMin_Cmd.bat @@ -10,7 +10,7 @@ REM :: Set up and run LY Python CMD prompt :: Sets up the DccScriptingInterface_Env, -:: Puts you in the CMD within the LY_DEV environment +:: Puts you in the CMD within the O3DE_DEV environment :: Set up window TITLE O3DE DCC Scripting Interface Py Qt Cmd @@ -39,7 +39,7 @@ echo _____________________________________________________________________ echo. :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% :: Create command prompt with environment CALL %windir%\system32\cmd.exe diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_VScode.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_VScode.bat similarity index 92% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_VScode.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_VScode.bat index 120584ed6a..2dc5b06032 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_VScode.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_VScode.bat @@ -61,14 +61,14 @@ echo ~ Launching DCCsi Project in VScode echo _____________________________________________________________________ echo. -echo LY_DEV = %LY_DEV% +echo O3DE_DEV = %O3DE_DEV% :: shared location for default O3DE python location -set DCCSI_PYTHON_INSTALL=%LY_DEV%\Python -echo DCCSI_PYTHON_INSTALL = %DCCSI_PYTHON_INSTALL% +set O3DE_PYTHON_INSTALL=%O3DE_DEV%\Python +echo O3DE_PYTHON_INSTALL = %O3DE_PYTHON_INSTALL% :: Wing and other IDEs probably prefer access directly to the python.exe -set DCCSI_PY_IDE = %DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python +set DCCSI_PY_IDE = %O3DE_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python echo DCCSI_PY_IDE = %DCCSI_PY_IDE% :: ide and debugger plug diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_WingIDE-7-1.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_WingIDE-7-1.bat similarity index 74% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_WingIDE-7-1.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_WingIDE-7-1.bat index c933670bab..56e278da08 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_WingIDE-7-1.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_WingIDE-7-1.bat @@ -20,6 +20,9 @@ COLOR 8E cd %~dp0 PUSHD %~dp0 +:: if the user has set up a custom env call it +IF EXIST "%~dp0Env_Dev.bat" CALL %~dp0Env_Dev.bat + :: Constant Vars (Global) :: global debug (propogates) IF "%DCCSI_GDEBUG%"=="" (set DCCSI_GDEBUG=True) @@ -51,35 +54,20 @@ echo. echo _____________________________________________________________________ echo. echo ~ WingIDE Version %DCCSI_WING_VERSION_MAJOR%.%DCCSI_WING_VERSION_MINOR% -echo ~ Launching O3DE %LY_PROJECT% project in WingIDE %DCCSI_WING_VERSION_MAJOR%.%DCCSI_WING_VERSION_MINOR% ... +echo ~ Launching O3DE %O3DE_PROJECT% project in WingIDE %DCCSI_WING_VERSION_MAJOR%.%DCCSI_WING_VERSION_MINOR% ... echo _____________________________________________________________________ echo. -echo LY_DEV = %LY_DEV% +echo O3DE_DEV = %O3DE_DEV% :: shared location for default O3DE python location -set DCCSI_PYTHON_INSTALL=%LY_DEV%\Python -echo DCCSI_PYTHON_INSTALL = %DCCSI_PYTHON_INSTALL% - -:: Wing and other IDEs probably prefer access directly to the python.exe -set DCCSI_PY_IDE = %DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python -echo DCCSI_PY_IDE = %DCCSI_PY_IDE% - -:: ide and debugger plug -set DCCSI_PY_BASE=%DCCSI_PY_IDE%\python.exe -echo DCCSI_PY_BASE = %DCCSI_PY_BASE% - -:: ide and debugger plug -set DCCSI_PY_DEFAULT=%DCCSI_PY_BASE% -echo DCCSI_PY_DEFAULT = %DCCSI_PY_DEFAULT% - -:: if the user has set up a custom env call it -IF EXIST "%~dp0Env_Dev.bat" CALL %~dp0Env_Dev.bat +set O3DE_PYTHON_INSTALL=%O3DE_DEV%\Python +echo O3DE_PYTHON_INSTALL = %O3DE_PYTHON_INSTALL% echo. :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% IF EXIST "%WINGHOME%\bin\wing.exe" ( start "" "%WINGHOME%\bin\wing.exe" "%WING_PROJ%" diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_mayaPy_2020.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_mayaPy_2020.bat similarity index 98% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_mayaPy_2020.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_mayaPy_2020.bat index f07ec093cb..33b8bc6392 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_mayaPy_2020.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_mayaPy_2020.bat @@ -48,7 +48,7 @@ echo MAYA_LOCATION = %MAYA_LOCATION% echo MAYA_BIN_PATH = %MAYA_BIN_PATH% :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% SETLOCAL ENABLEDELAYEDEXPANSION diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_mayapy_WingIDE-7-1.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_mayapy_WingIDE-7-1.bat similarity index 87% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_mayapy_WingIDE-7-1.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_mayapy_WingIDE-7-1.bat index 631250d06b..ca5b175c39 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_mayapy_WingIDE-7-1.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_mayapy_WingIDE-7-1.bat @@ -50,19 +50,19 @@ echo. echo _____________________________________________________________________ echo. echo ~ WingIDE Version %DCCSI_WING_VERSION_MAJOR%.%DCCSI_WING_VERSION_MINOR% -echo ~ Launching O3DE %LY_PROJECT% project in WingIDE %DCCSI_WING_VERSION_MAJOR%.%DCCSI_WING_VERSION_MINOR% ... +echo ~ Launching O3DE %O3DE_PROJECT% project in WingIDE %DCCSI_WING_VERSION_MAJOR%.%DCCSI_WING_VERSION_MINOR% ... echo ~ MayaPy.exe (default python interpreter) echo _____________________________________________________________________ echo. -echo LY_DEV = %LY_DEV% +echo O3DE_DEV = %O3DE_DEV% :: shared location for default O3DE python location -set DCCSI_PYTHON_INSTALL=%LY_DEV%\Python -echo DCCSI_PYTHON_INSTALL = %DCCSI_PYTHON_INSTALL% +set O3DE_PYTHON_INSTALL=%O3DE_DEV%\Python +echo O3DE_PYTHON_INSTALL = %O3DE_PYTHON_INSTALL% :: Wing and other IDEs probably prefer access directly to the python.exe -set DCCSI_PY_IDE = %DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python +set DCCSI_PY_IDE = %O3DE_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python echo DCCSI_PY_IDE = %DCCSI_PY_IDE% :: ide and debugger plug @@ -79,7 +79,7 @@ IF EXIST "%~dp0Env_Dev.bat" CALL %~dp0Env_Dev.bat echo. :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% IF EXIST "%WINGHOME%\bin\wing.exe" ( start "" "%WINGHOME%\bin\wing.exe" "%WING_PROJ%" diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_pyBASE.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_pyBASE.bat similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_pyBASE.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_pyBASE.bat diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_pyBASE_Cmd.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_pyBASE_Cmd.bat similarity index 97% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_pyBASE_Cmd.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_pyBASE_Cmd.bat index 9f8950f59f..e1b075e065 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_pyBASE_Cmd.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_pyBASE_Cmd.bat @@ -33,7 +33,7 @@ CALL %~dp0\Env_Maya.bat IF EXIST "%~dp0Env_Dev.bat" CALL %~dp0Env_Dev.bat :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% :: Create command prompt with environment CALL %windir%\system32\cmd.exe diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/README.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/README.txt new file mode 100644 index 0000000000..e5b7946276 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/README.txt @@ -0,0 +1,78 @@ +""" +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 +""" +# ------------------------------------------------------------------------- + +DccScriptingInterface (DCCsi) is a framework for O3DE extensions, for example: +- Lightweight python integrations with DCC tools like Maya +- O3DE configuration, customization and extensions of tools +- Standalone PySide2/Qt tools +- ^ These might utilize a mix of O3DE and DDC python APIs + +The DccScriptingInterface\config.py, procedurally provides a synthetic env context. +This env is a data-driven approach to configuring layered and managed env settings. + +This env provides the hooks for DDC apps and/or standalone tools, +to configure acess to O3DE code (for boostrapping), safely retreive known paths, set/get developer flags, etc. + +DccScriptingInterface\Tools\Dev\Windows\ + +This is a .bat file based version of the default env context for development on windows. +This is what we use to boot the default env context such that it is available, when launching a development tool such as a IDE. + +This allows a developer to troubleshoot/debug code, like config.py + +Other tools, can use config.py to stand up the env context. + +What is in this folder ... + +Core env modules +--------------------- +Env_Core.bat : core access to O3DE and DCCsi +Env_Python.bat : access to O3DE python and general py configuration +Env_Qt.bat : access to O3DE Qt .dll files and PySide2 + +DCC add on envars +--------------------- +Env_Maya.bat : configures Maya with code O3DE/DCCsi access +Env_Substance.bat : Configures Substance Designer + +IDE env +--------------------- +Env_WingIDE.bat : configures WingIDE for DCCsi development +Env_VScode.bat : configures VScode for DCCsi development +Env_PyCharm.bat : configures PyCharm for DCCsi development + +Launchers +--------------------- +Launch_env_Cmd.bat : Starts a cmd with entire managed env context + : ^ allows use to validate env + : ^ display all default ENVAR plugs + : ^ allows user to test O3DE python + scripts from cmd +Launch_PyMin_Cmd.bat : Starts minimal cmd with O3DE python access only + : ^ for instance, test Dccsi\config.py like this: + : {DCCsi prommpt}>python config.py +Launch_Maya_2020.bat : Starts Maya2020 within managed env context +Launch_WindIDE-7-1.bat : Starts WingIDE within managed env context + +--------------------- +Instructions: How to test the synthetic environment and settings externally + +1. Run the cmd: Launch_PyMin_Cmd.bat + + C:\Depot\o3de-engine\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface> + +2. Run command:>python config.py -dm=True -py=True -qt=True + +What this does? +- runs the O3DE python exe +- starts the config.py which begins to procedurally create synthetic/dynamic environment (hooks) +- ^ this starts with DCCsi hooks +- enters 'dev mode'(-dm) and attempts to attach debugger (Wing IDE only for now, others planned) +- enables additional O3DE python hooks and code access +- ^ great for standalone tools, but you don't want that functionality to interfer with other DCC tools python environments (like Maya!) +- enables access to O3DE Qt .dlls and PySide2 python package support(-qt) +- ^ great for standalone PySide2 which can operate outside of the O3DE editor \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Setuo_copy_oiio.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Setuo_copy_oiio.bat similarity index 76% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Setuo_copy_oiio.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Setuo_copy_oiio.bat index 590e634d54..18a58e1371 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Setuo_copy_oiio.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Setuo_copy_oiio.bat @@ -13,12 +13,12 @@ cd %~dp0 PUSHD %~dp0 :: This maps up to the \Dev folder -set LY_DEV=..\..\..\..\..\.. +set O3DE_DEV=..\..\..\..\..\.. :: shared location for default O3DE python location -set DCCSI_PYTHON_INSTALL=%LY_DEV%\Python +set O3DE_PYTHON_INSTALL=%O3DE_DEV%\Python -set PY_SITE=%DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python\Lib\site-packages +set PY_SITE=%O3DE_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python\Lib\site-packages set PACKAGE_LOC=C:\Depot\3rdParty\packages\openimageio-2.1.16.0-rev1-windows\OpenImageIO\2.1.16.0\win_x64\bin diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.dev/readme.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.dev/readme.txt similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.dev/readme.txt rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.dev/readme.txt diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.dev/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.dev/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.gitignore b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.gitignore similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.gitignore rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.gitignore diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/.gitignore b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/.gitignore similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/.gitignore rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/.gitignore diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/.p4ignore b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/.p4ignore similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/.p4ignore rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/.p4ignore diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/DccScriptingInterface.iml b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/DccScriptingInterface.iml similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/DccScriptingInterface.iml rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/DccScriptingInterface.iml diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/encodings.xml b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/encodings.xml similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/encodings.xml rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/encodings.xml diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/main.py similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/main.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/main.py diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/misc.xml b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/misc.xml similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/misc.xml rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/misc.xml diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/modules.xml b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/modules.xml similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/modules.xml rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/modules.xml diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/vcs.xml b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/vcs.xml similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/vcs.xml rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/vcs.xml diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/webResources.xml b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/webResources.xml similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/webResources.xml rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/webResources.xml diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.vscode/dccsi.code-workspace b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.vscode/dccsi.code-workspace similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.vscode/dccsi.code-workspace rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.vscode/dccsi.code-workspace diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.vscode/launch.json b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.vscode/launch.json similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.vscode/launch.json rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.vscode/launch.json diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.vscode/settings.json b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.vscode/settings.json similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.vscode/settings.json rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.vscode/settings.json diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.wing/.gitignore b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.wing/.gitignore similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.wing/.gitignore rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.wing/.gitignore diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.wing/DCCsi_7x.wpr b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.wing/DCCsi_7x.wpr similarity index 57% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.wing/DCCsi_7x.wpr rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.wing/DCCsi_7x.wpr index aa7a8c4bd9..18af1298db 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.wing/DCCsi_7x.wpr +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.wing/DCCsi_7x.wpr @@ -5,14 +5,14 @@ ################################################################## [project attributes] debug.launch-configs = (2, - {'launch-GeaM41WYMGA1sEfm': ({'shared': True}, + {'launch-0L6se5pxC9AWvpY0': ({'shared': True}, {'buildcmd': ('project', None), - 'env': ('custom', + 'env': ('project', [u'']), - 'name': u'DCCSI_PY_MAYA', + 'name': 'DCCSI_PY_IDE', 'pyexec': ('custom', - u'${DCCSI_PY_MAYA}'), + u'${DCCSI_PY_IDE}'), 'pypath': ('project', []), 'pyrunargs': ('project', @@ -20,14 +20,14 @@ debug.launch-configs = (2, 'runargs': u'', 'rundir': ('project', u'')}), - 'launch-WUN9lgYK6qYU7qE9': ({'shared': True}, + 'launch-GeaM41WYMGA1sEfm': ({'shared': True}, {'buildcmd': ('project', None), 'env': ('custom', [u'']), - 'name': u'DCCSI_PY_BASE', + 'name': u'DCCSI_PY_MAYA', 'pyexec': ('custom', - u'${DCCSI_PY_BASE}'), + u'${DCCSI_PY_MAYA}'), 'pypath': ('project', []), 'pyrunargs': ('project', @@ -35,14 +35,14 @@ debug.launch-configs = (2, 'runargs': u'', 'rundir': ('project', u'')}), - 'launch-oobMrvXFf1SwtYBg': ({'shared': True}, + 'launch-WUN9lgYK6qYU7qE9': ({'shared': True}, {'buildcmd': ('project', None), 'env': ('custom', [u'']), - 'name': u'DCCSI_PY_DEFAULT', + 'name': u'DCCSI_PY_BASE', 'pyexec': ('custom', - u'${DCCSI_PY_DEFAULT}'), + u'${DCCSI_PY_BASE}'), 'pypath': ('project', []), 'pyrunargs': ('project', @@ -50,35 +50,28 @@ debug.launch-configs = (2, 'runargs': u'', 'rundir': ('project', u'')})}) -proj.directory-list = [{'dirloc': loc('../..'), +proj.directory-list = [{'dirloc': loc('../../../../..'), 'excludes': (), 'filter': u'*', - 'include_hidden': True, + 'include_hidden': False, + 'recursive': True, + 'watch_for_changes': True}, + {'dirloc': loc('../../../../../../../../Atom/Feature/Common/Editor/Scripts/ColorGrading'), + 'excludes': (), + 'filter': u'*', + 'include_hidden': False, + 'recursive': True, + 'watch_for_changes': True}, + {'dirloc': loc('../../../../../../../../../python'), + 'excludes': (), + 'filter': u'*', + 'include_hidden': False, 'recursive': True, 'watch_for_changes': True}] proj.file-type = 'shared' -proj.home-dir = loc('../..') -proj.launch-config = {loc('../../SDK/Atom/Scripts/Python/DCC_Materials/maya_materials_export.py'): ('c'\ - 'ustom', - (u'', - 'launch-GeaM41WYMGA1sEfm')), - loc('../../SDK/Maya/Scripts/Python/legacy_asset_converter/main.py'): ('c'\ - 'ustom', - (u'', - 'launch-WUN9lgYK6qYU7qE9')), - loc('../../azpy/__init__.py'): ('custom', - (u'', - 'launch-oobMrvXFf1SwtYBg')), - loc('../../azpy/constants.py'): ('custom', - (u'', - 'launch-GeaM41WYMGA1sEfm')), - loc('../../azpy/env_base.py'): ('project', - (u'', - 'launch-GeaM41WYMGA1sEfm')), - loc('../../azpy/maya/callbacks/node_message_callback_handler.py'): ('c'\ - 'ustom', +proj.launch-config = {loc('../../../../../azpy/core/py2/utils.py'): ('custom', (u'', - 'launch-GeaM41WYMGA1sEfm')), - loc('../../config.py'): ('custom', + 'launch-0L6se5pxC9AWvpY0')), + loc('../../../../../azpy/core/py3/utils.py'): ('custom', (u'', - 'launch-GeaM41WYMGA1sEfm'))} + 'launch-0L6se5pxC9AWvpY0'))} diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/readme.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/readme.txt similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/readme.txt rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/readme.txt diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/README.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/README.txt new file mode 100644 index 0000000000..05aa948943 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/README.txt @@ -0,0 +1,9 @@ +""" +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 folder resprents a Mock Tool - currently just a scaffold (not implemented) \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/Resources/resource.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/Resources/resource.txt new file mode 100644 index 0000000000..30df2e0eb9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/Resources/resource.txt @@ -0,0 +1,9 @@ +""" +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 folder is for tool resources - this file is a resource stub. \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/config.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/config.py new file mode 100644 index 0000000000..1dc43d8485 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/config.py @@ -0,0 +1,12 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- + +print('Not Implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/main.py new file mode 100644 index 0000000000..1dc43d8485 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/main.py @@ -0,0 +1,12 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- + +print('Not Implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/settings.json b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/settings.json new file mode 100644 index 0000000000..d58fc7e10c --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/settings.json @@ -0,0 +1,17 @@ +{ + "default": { + "DCCSI_GDEBUG": true, + "SPDX-LICENSE-IDENTIFIER": "Apache-2.0 OR MIT", + "COPYWRITE":"Copyright (c) Contributors to the Open 3D Engine Project.", + "COPYWRITE_MSG":"For complete copyright and license terms please see the LICENSE at the root of this distribution." + }, + "development": { + "DCCSI_GDEBUG": true, + "TEST_RULE": "/dccsi_with_json", + "MESSAGE": "The O3DE DCCsi manages dynamic configuration and settings with dynaconf", + "DYNACONF_HELP": "https://dynaconf.readthedocs.io/" + }, + "production": { + "DCCSI_GDEBUG": false + } +} \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/Tests/OpenImageIO/AA_Gun_01_ddna.tif b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/Tests/OpenImageIO/AA_Gun_01_ddna.tif new file mode 100644 index 0000000000..0cefbe9c25 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/Tests/OpenImageIO/AA_Gun_01_ddna.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c34bbeafae5c5b62d3571ac9eee295fff12ac0744f81375f53a3faf2c5095e4 +size 286400 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/Tests/OpenImageIO/test_oiio.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/Tests/OpenImageIO/test_oiio.py new file mode 100644 index 0000000000..b33fa8ef56 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/Tests/OpenImageIO/test_oiio.py @@ -0,0 +1,66 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- + +import OpenImageIO as oiio + +def convert(src): + try: + rgba = oiio.ImageBuf(src) + spec = get_image_spec(rgba) + + # Normal output ------> + rgb = oiio.ImageBufAlgo.channels(rgba, (0, 1, 2)) + normal_output = src.replace('ddna', 'Normal') + + # Roughness output ------> + alpha = oiio.ImageBufAlgo.channels(rgba, (3,)) + roughness = oiio.ImageBufAlgo.invert(alpha) + roughness_output = normal_output.replace('Normal', 'Roughness') + + write_image(rgba, normal_output, spec['format']) + write_image(roughness, roughness_output, spec['format']) + # logging.info('Output Normal Map: {}'.format(normal_output)) + # logging.info('Output Roughness Map: {}'.format(roughness_output)) + + except Exception as e: + logging.info('OIIO error in image conversion: {}'.format(e)) + return None + +def get_image_spec(target_image): + spec = target_image.spec() + info = {'resolution': (spec.width, spec.height, spec.x, spec.y), 'channels': spec.channelnames, + 'format': str(spec.format)} + if spec.channelformats : + info['channelformats'] = str(spec.channelformats) + info['alpha channel'] = str(spec.alpha_channel) + info['z channel'] = str(spec.z_channel) + info['deep'] = str(spec.deep) + for i in range(len(spec.extra_attribs)): + if type(spec.extra_attribs[i].value) == str: + info[spec.extra_attribs[i].name] = spec.extra_attribs[i].value + else: + info[spec.extra_attribs[i].name] = spec.extra_attribs[i].value + return info + +def write_image(image, filename, image_format): + if not image.has_error: + image.set_write_format(image_format) + image.write(filename) + if image.has_error: + print("Error writing", filename, ":", image.geterror()) + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + # run a test + convert('AA_Gun_01_ddna.tif') + # validate() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Resources/Atom/StandardPBR_AllProperties.material b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Resources/Atom/StandardPBR_AllProperties.material new file mode 100644 index 0000000000..7d38d1a1a3 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Resources/Atom/StandardPBR_AllProperties.material @@ -0,0 +1,47 @@ +{ + "description": "", + "materialType": "Materials/Types/StandardPBR.materialtype", + "parentMaterial": "", + "propertyLayoutVersion": 3, + "properties": { + "general": { + "texcoord": 0 + }, + "baseColor": { + "colorLinear": [ 1.0, 1.0, 1.0 ], + "factor": 1.0, + "useTexture": true, + "textureMap": "EngineAssets/TextureMsg/DefaultNoUVs.tif" + }, + "metallic": { + "factor": 0.0, + "useTexture": false, + "textureMap": "" + }, + "roughness": { + "factor": 1.0, + "useTexture": true, + "textureMap": "EngineAssets/TextureMsg/DefaultNoUVs_spec.tif" + }, + "specularF0": { + "factor": 0.5, + "useTexture": false, + "textureMap": "" + }, + "normal": { + "factor": 1.0, + "useTexture": true, + "textureMap": "EngineAssets/TextureMsg/DefaultNoUVs_ddn.tif" + }, + "opacity": { + "doubleSided": false, + "factor": 1.0, + "cutoutAlpha": false, + "cutoutThreshold": 0.5, + "useBaseColorTextureAlpha": false, + "useTexture": false, + "textureMap": "" + } + } +} + diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/start_service.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/start_service.py new file mode 100644 index 0000000000..ac9d02cf5d --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/start_service.py @@ -0,0 +1,21 @@ +""" +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 +""" +# ------------------------------------------------------------------------- +"""DCCsi Tool and Application service launcher""" + +def start_service(): + """Not Implemented""" + print('DCCsi.Tools.DCC.start_service() not implemented') + return None + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + """Run this file as main""" + + start_service() \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/__init__.py index 0577c50474..958508c227 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/__init__.py @@ -26,26 +26,18 @@ import logging as _logging # ------------------------------------------------------------------------- -_ORG_TAG = 'Amazon_Lumberyard' -_APP_TAG = 'DCCsi' -_TOOL_TAG = 'azpy' -_TYPE_TAG = 'module' +# global scope +_PACKAGENAME = 'azpy' + +__all__ = ['config_utils', + 'constants', + 'env_bool', + 'return_stub', + 'core', + 'dcc', + 'dev', + 'test'] -_PACKAGENAME = _TOOL_TAG - -__all__ = ['config_utils', 'render', - 'constants', 'return_stub', 'synthetic_env', - 'env_base', 'env_bool', 'test', 'dev', - 'lumberyard', 'marmoset'] # 'blender', 'maya', 'substance', 'houdini'] -# ------------------------------------------------------------------------- - - -# ------------------------------------------------------------------------- -# _ROOT_LOGGER = _logging.getLogger() # only use this if debugging -# https://stackoverflow.com/questions/56733085/how-to-know-the-current-file-path-after-being-frozen-into-an-executable-using-cx/56748839 -#os.chdir(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))) -# ------------------------------------------------------------------------- -# global space # we need to set up basic access to the DCCsi _MODULE_PATH = os.path.realpath(__file__) # To Do: what if frozen? _DCCSIG_PATH = os.path.normpath(os.path.join(_MODULE_PATH, '../..')) @@ -58,8 +50,11 @@ import azpy.env_bool as env_bool import azpy.constants as constants import azpy.config_utils as config_utils -_G_DEBUG = env_bool.env_bool(constants.ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool.env_bool(constants.ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool.env_bool(constants.ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_LOGLEVEL = int(env_bool.env_bool(constants.ENVAR_DCCSI_LOGLEVEL, int(20))) +if _DCCSI_GDEBUG: + _DCCSI_LOGLEVEL = int(10) # for py2.7 (Maya) we provide this, so we must assume some bootstrapping # has occured, see DccScriptingInterface\\config.py (_DCCSI_PYTHON_LIB_PATH) @@ -69,38 +64,44 @@ try: except: import pathlib2 as pathlib from pathlib import Path -if _G_DEBUG: - print('DCCsi debug breadcrumb, pathlib is: {}'.format(pathlib)) +if _DCCSI_GDEBUG: + print('[DCCsi][AZPY] DEBUG BREADCRUMB, pathlib is: {}'.format(pathlib)) +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# set up module logging +#for handler in _logging.root.handlers[:]: + #_logging.root.removeHandler(handler) +_logging.basicConfig(format=constants.FRMT_LOG_LONG, level=_DCCSI_LOGLEVEL) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) +# ------------------------------------------------------------------------- -# to be continued... +# ------------------------------------------------------------------------- # get/set the project name -_LY_DEV = os.getenv(constants.ENVAR_LY_DEV, +_O3DE_DEV = Path(os.getenv(constants.ENVAR_O3DE_DEV, config_utils.get_stub_check_path(in_path=os.getcwd(), - check_stub='engine.json')) + check_stub='engine.json'))) +_LOGGER.debug('_O3DE_DEV" {}'.format(_O3DE_DEV.resolve())) + +_O3DE_PROJECT_PATH = Path(os.getenv(constants.ENVAR_O3DE_PROJECT_PATH, + config_utils.get_o3de_project_path())) +_LOGGER.debug('_O3DE_PROJECT_PATH" {}'.format(_O3DE_PROJECT_PATH.resolve())) # get/set the project name -_LY_PROJECT_NAME = os.getenv(constants.ENVAR_LY_PROJECT, - config_utils.get_current_project().name) +if _O3DE_PROJECT_PATH: + _O3DE_PROJECT = str(os.getenv(constants.ENVAR_O3DE_PROJECT, + _O3DE_PROJECT_PATH.name)) +else: + _O3DE_PROJECT='o3de' # project cache log dir path -_DCCSI_LOG_PATH = Path(os.getenv(constants.ENVAR_DCCSI_LOG_PATH, - Path(_LY_DEV, - _LY_PROJECT_NAME, - 'Cache', - 'pc', 'user', 'log', 'logs'))) - - -for handler in _logging.root.handlers[:]: - _logging.root.removeHandler(handler) - -# very basic root logger for early debugging, flip to while 1: -while 0: - _logging.basicConfig(level=_logging.DEBUG, - format=constants.FRMT_LOG_LONG, - datefmt='%m-%d %H:%M') - - _logging.debug('azpy.rootlogger> root logger set up for debugging') # root logger +from azpy.constants import TAG_DCCSI_NICKNAME +from azpy.constants import PATH_DCCSI_LOG_PATH +_DCCSI_LOG_PATH = Path(PATH_DCCSI_LOG_PATH.format(O3DE_PROJECT_PATH=_O3DE_PROJECT_PATH.resolve(), + TAG_DCCSI_NICKNAME=TAG_DCCSI_NICKNAME)) # ------------------------------------------------------------------------- @@ -145,14 +146,15 @@ if sys.version_info.major < 3: # ------------------------------------------------------------------------- def initialize_logger(name, log_to_file=False, - default_log_level=_logging.NOTSET): + default_log_level=_logging.NOTSET, + propogate=False): """Start a azpy logger""" _logger = _logging.getLogger(name) - _logger.propagate = False + _logger.propagate = propogate if not _logger.handlers: _log_level = int(os.getenv('DCCSI_LOGLEVEL', default_log_level)) - if _G_DEBUG: + if _DCCSI_GDEBUG: _log_level = int(10) # force when debugging print('_log_level: {}'.format(_log_level)) @@ -201,27 +203,21 @@ def initialize_logger(name, return _logger # ------------------------------------------------------------------------- -# ------------------------------------------------------------------------- -# set up logger with both console and file _logging -if _G_DEBUG: - _LOGGER = initialize_logger(_PACKAGENAME, log_to_file=True) -else: - _LOGGER = initialize_logger(_PACKAGENAME, log_to_file=False) - -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# ------------------------------------------------------------------------- # some simple logger tests # evoke the filehandlers and test writting to the log file -if _G_DEBUG: +if _DCCSI_GDEBUG: _LOGGER.info('Forced Info! for {0}.'.format({_PACKAGENAME})) _LOGGER.error('Forced ERROR! for {0}.'.format({_PACKAGENAME})) # debug breadcrumbs to check this module and used paths _LOGGER.debug('MODULE_PATH: {}'.format(_MODULE_PATH)) -_LOGGER.debug('LY_DEV_PATH: {}'.format(_LY_DEV)) +_LOGGER.debug('O3DE_DEV_PATH: {}'.format(_O3DE_DEV)) _LOGGER.debug('DCCSI_PATH: {}'.format(_DCCSIG_PATH)) -_LOGGER.debug('LY_PROJECT_TAG: {}'.format(_LY_PROJECT_NAME)) +_LOGGER.debug('O3DE_PROJECT_TAG: {}'.format(_O3DE_PROJECT)) _LOGGER.debug('DCCSI_LOG_PATH: {}'.format(_DCCSI_LOG_PATH)) +# ------------------------------------------------------------------------- # ------------------------------------------------------------------------- @@ -258,9 +254,9 @@ del _LOGGER # Main Code Block, runs this script as main (testing) # ------------------------------------------------------------------------- if __name__ == '__main__': - _G_DEBUG = True + _DCCSI_GDEBUG = True _DCCSI_DEV_MODE = True - if _G_DEBUG: + if _DCCSI_GDEBUG: print(_DCCSIG_PATH) test_imports() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/config_utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/config_utils.py index 2df85cbfaa..f6b71a7d97 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/config_utils.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/config_utils.py @@ -18,14 +18,31 @@ import logging as _logging # -------------------------------------------------------------------------- -_PACKAGENAME = 'azpy.config_utils' +# note: this module is called in other root modules +# must avoid cyclical imports +# global scope +# normally would pull the constant envar string +# but avoiding cyclical imports here FRMT_LOG_LONG = "[%(name)s][%(levelname)s] >> %(message)s (%(asctime)s; %(filename)s:%(lineno)d)" -_logging.basicConfig(level=_logging.INFO, - format=FRMT_LOG_LONG, - datefmt='%m-%d %H:%M') -_LOGGER = _logging.getLogger(_PACKAGENAME) -_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) +from azpy.env_bool import env_bool +_DCCSI_GDEBUG = env_bool('DCCSI_GDEBUG', False) +_DCCSI_LOGLEVEL = env_bool('DCCSI_LOGLEVEL', False) +_DCCSI_LOGLEVEL = int(env_bool('DCCSI_LOGLEVEL', int(20))) +if _DCCSI_GDEBUG: + _DCCSI_LOGLEVEL = int(10) + +_MODULENAME = __name__ +if _MODULENAME is '__main__': + _MODULENAME = 'azpy.config_utils' + +# set up module logging +#for handler in _logging.root.handlers[:]: + #_logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_MODULENAME) +#_logging.basicConfig(format=FRMT_LOG_LONG, level=_DCCSI_LOGLEVEL) +_LOGGER.propagate = False +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) __all__ = ['get_os', 'return_stub', 'get_stub_check_path', 'get_dccsi_config', 'get_current_project'] @@ -34,13 +51,15 @@ __all__ = ['get_os', 'return_stub', 'get_stub_check_path', # ------------------------------------------------------------------------- # just a quick check to ensure what paths have code access -_G_DEBUG = False # enable for debug prints -if _G_DEBUG: +if _DCCSI_GDEBUG: known_paths = list() for p in sys.path: known_paths.append(p) _LOGGER.debug(known_paths) +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- # this import can fail in Maya 2020 (and earlier) stuck on py2.7 # wrapped in a try, to trap and providing messaging to help user correct try: @@ -76,6 +95,15 @@ def get_os(): # ------------------------------------------------------------------------- +# ------------------------------------------------------------------------- +from azpy.core import get_datadir +# there was a method here refactored out to add py2.7 support for Maya 2020 +#"DccScriptingInterface\azpy\core\py2\utils.py get_datadir()" +#"DccScriptingInterface\azpy\core\py3\utils.py get_datadir()" +# Warning: planning to deprecate py2 support +# ------------------------------------------------------------------------- + + # ------------------------------------------------------------------------- def return_stub_dir(stub_file='dccsi_stub'): _dir_to_last_file = None @@ -126,6 +154,30 @@ def get_stub_check_path(in_path=os.getcwd(), check_stub='engine.json'): # ------------------------------------------------------------------------- +# ------------------------------------------------------------------------- +def get_o3de_engine_root(check_stub='engine.json'): + # get the O3DE engine root folder + # if we are running within O3DE we can ensure which engine is running + _O3DE_DEV = None + try: + import azlmbr # this file will fail outside of O3DE + except ImportError as e: + # if that fails, we can search up + # search up to get \dev + _O3DE_DEV = get_stub_check_path(check_stub='engine.json') + # To Do: What if engine.json doesn't exist? + else: + # execute if no exception + # allow for external ENVAR override + from azpy.constants import ENVAR_O3DE_DEV + _O3DE_DEV = Path(os.getenv(ENVAR_O3DE_DEV, azlmbr.paths.engroot)) + finally: + # note: can't use fstrings as this module gets called with py2.7 in maya + _LOGGER.info('O3DE engine root: {}'.format(_O3DE_DEV.resolve())) + return _O3DE_DEV +# ------------------------------------------------------------------------- + + # ------------------------------------------------------------------------- # settings.setenv() # doing this will add the additional DYNACONF_ envars def get_dccsi_config(dccsi_dirpath=return_stub_dir()): @@ -176,46 +228,75 @@ def get_current_project_cfg(dev_folder=get_stub_check_path()): # ------------------------------------------------------------------------- -def get_current_project(): +def get_check_global_project(): """Gets o3de project via .o3de data in user directory""" from azpy.constants import PATH_USER_O3DE_BOOTSTRAP from collections import OrderedDict from box import Box + from azpy.core import get_datadir bootstrap_box = None - - try: - bootstrap_box = Box.from_json(filename=str(Path(PATH_USER_O3DE_BOOTSTRAP).resolve()), - encoding="utf-8", - errors="strict", - object_pairs_hook=OrderedDict) - except Exception as e: - # this file runs in py2.7 for Maya 2020, FileExistsError is not defined - _LOGGER.error('FileExistsError: {}'.format(PATH_USER_O3DE_BOOTSTRAP)) - _LOGGER.error('exception is: {}'.format(e)) - + json_file_path = Path(PATH_USER_O3DE_BOOTSTRAP) + if json_file_path.exists(): + try: + bootstrap_box = Box.from_json(filename=str(json_file_path.resolve()), + encoding="utf-8", + errors="strict", + object_pairs_hook=OrderedDict) + except IOError as e: + # this file runs in py2.7 for Maya 2020, FileExistsError is not defined + _LOGGER.error('Bad file interaction: {}'.format(json_file_path.resolve())) + _LOGGER.error('Exception is: {}'.format(e)) + pass if bootstrap_box: # this seems fairly hard coded - what if the data changes? project_path=Path(bootstrap_box.Amazon.AzCore.Bootstrap.project_path) - return project_path.resolve() + return project_path else: return None # ------------------------------------------------------------------------- +# ------------------------------------------------------------------------- +def get_o3de_project_path(): + """figures out the o3de project path + if not found defaults to the engine folder""" + _O3DE_PROJECT_PATH = None + try: + import azlmbr # this file will fail outside of O3DE + except ImportError as e: + # (fallback 1) this checks if a global project is set + # This check user home for .o3de data + _O3DE_PROJECT_PATH = get_check_global_project() + else: + # execute if no exception, this would indicate we are in O3DE land + # allow for external ENVAR override + from azpy.constants import ENVAR_O3DE_PROJECT_PATH + _O3DE_PROJECT_PATH = Path(os.getenv(ENVAR_O3DE_PROJECT_PATH, azlmbr.paths.projectroot)) + finally: + # (fallback 2) if None, fallback to engine folder + if not _O3DE_PROJECT_PATH: + _O3DE_PROJECT_PATH = get_o3de_engine_root() + # note: can't use fstrings as this module gets called with py2.7 in maya + _LOGGER.info('O3DE project root: {}'.format(_O3DE_PROJECT_PATH.resolve())) + return _O3DE_PROJECT_PATH +# ------------------------------------------------------------------------- + + # ------------------------------------------------------------------------- def bootstrap_dccsi_py_libs(dccsi_dirpath=return_stub_dir()): """Builds and adds local site dir libs based on py version""" from azpy.constants import STR_DCCSI_PYTHON_LIB_PATH # a path string constructor - _DCCSI_PYTHON_LIB_PATH = STR_DCCSI_PYTHON_LIB_PATH.format(dccsi_dirpath, - sys.version_info[0], - sys.version_info[1]) - - if os.path.exists(_DCCSI_PYTHON_LIB_PATH): - _LOGGER.debug('Performed site.addsitedir({})'.format(_DCCSI_PYTHON_LIB_PATH)) - site.addsitedir(_DCCSI_PYTHON_LIB_PATH) # PYTHONPATH + _DCCSI_PYTHON_LIB_PATH = Path(STR_DCCSI_PYTHON_LIB_PATH.format(dccsi_dirpath, + sys.version_info[0], + sys.version_info[1])) + + if _DCCSI_PYTHON_LIB_PATH.exists(): + site.addsitedir(_DCCSI_PYTHON_LIB_PATH.resolve()) # PYTHONPATH + _LOGGER.debug('Performed site.addsitedir({})' + ''.format(_DCCSI_PYTHON_LIB_PATH.resolve())) return _DCCSI_PYTHON_LIB_PATH else: message = "Doesn't exist: {}".format(_DCCSI_PYTHON_LIB_PATH) @@ -243,13 +324,10 @@ if __name__ == '__main__': _config = get_dccsi_config() _LOGGER.info('DCCSI_CONFIG_PATH: {}'.format(_config)) - _LOGGER.info('LY_DEV: {}'.format(get_stub_check_path('engine.json'))) - - # this will be deprecated and shouldn't work soon (returns None) - _LOGGER.info('LY_PROJECT: {}'.format(get_current_project_cfg(get_stub_check_path('bootstrap.cfg')))) + _LOGGER.info('O3DE_DEV: {}'.format(get_o3de_engine_root(check_stub='engine.json'))) # new o3de version - _LOGGER.info('LY_PROJECT: {}'.format(get_current_project())) + _LOGGER.info('O3DE_PROJECT: {}'.format(get_check_global_project())) _LOGGER.info('DCCSI_PYTHON_LIB_PATH: {}'.format(bootstrap_dccsi_py_libs(return_stub_dir('dccsi_stub')))) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/constants.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/constants.py index b9619fa584..d23601a33d 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/constants.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/constants.py @@ -25,7 +25,16 @@ import sys import site from os.path import expanduser import logging as _logging +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# global scope +_MODULENAME = __name__ +if _MODULENAME is '__main__': + _MODULENAME = 'azpy.constants' +os.environ['PYTHONINSPECT'] = 'True' # for this module to perform standalone # we need to set up basic access to the DCCsi _MODULE_PATH = os.path.realpath(__file__) # To Do: what if frozen? @@ -33,10 +42,10 @@ _DCCSIG_PATH = os.path.normpath(os.path.join(_MODULE_PATH, '../..')) _DCCSIG_PATH = os.getenv('DCCSIG_PATH', _DCCSIG_PATH) site.addsitedir(_DCCSIG_PATH) -# azpy module -#import azpy.constants as cnst +# now we have azpy api access +import azpy +from azpy.env_bool import env_bool from azpy.config_utils import return_stub_dir -import azpy.env_bool as env_bool # ------------------------------------------------------------------------- @@ -49,24 +58,26 @@ ENVAR_DCCSI_LOGLEVEL = str('DCCSI_LOGLEVEL') # Log formating FRMT_LOG_LONG = "[%(name)s][%(levelname)s] >> %(message)s (%(asctime)s; %(filename)s:%(lineno)d)" FRMT_LOG_SHRT = "[%(asctime)s][%(name)s][%(levelname)s] >> %(message)s" +# ------------------------------------------------------------------------- -# global space -_G_DEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) -for handler in _logging.root.handlers[:]: - _logging.root.removeHandler(handler) +# ------------------------------------------------------------------------- +# global debug stuff +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_LOGLEVEL = int(env_bool(ENVAR_DCCSI_LOGLEVEL, int(20))) +if _DCCSI_GDEBUG: + _DCCSI_LOGLEVEL = int(10) +# ------------------------------------------------------------------------- -_PACKAGENAME = 'azpy.constants' -_LOG_LEVEL = int(20) -if _G_DEBUG: - _LOG_LEVEL = int(10) -_logging.basicConfig(level=_LOG_LEVEL, - format=FRMT_LOG_LONG, - datefmt='%m-%d %H:%M') -_LOGGER = _logging.getLogger(_PACKAGENAME) -_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) +# ------------------------------------------------------------------------- +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_MODULENAME) +_logging.basicConfig(format=FRMT_LOG_LONG, level=_DCCSI_LOGLEVEL) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) # ------------------------------------------------------------------------- @@ -80,24 +91,25 @@ STR_CROSSBAR_RL = str('{0}\r'.format(STR_CROSSBAR)) STR_CROSSBAR_NL = str('{0}\n'.format(STR_CROSSBAR)) # some common str tags -TAG_DEFAULT_COMPANY = str('Amazon.Lumberyard') +TAG_DEFAULT_COMPANY = str('Amazon.O3DE') TAG_DEFAULT_PROJECT = str('DccScriptingInterface') +TAG_DCCSI_NICKNAME = str('DCCsi') TAG_MOCK_PROJECT = str('MockProject') -TAG_DIR_LY_DEV = str('dev') +TAG_DIR_O3DE_DEV = str('dev') TAG_DIR_DCCSI_AZPY = str('azpy') -TAG_DIR_DCCSI_SDK = str('SDK') -TAG_DIR_LY_BUILD = str('build') +TAG_DIR_DCCSI_TOOLS = str('Tools') +TAG_DIR_O3DE_BUILD_FOLDER = str('build') TAG_QT_PLUGIN_PATH = str('QT_PLUGIN_PATH') TAG_O3DE_FOLDER = str('.o3de') TAG_O3DE_BOOTSTRAP = str('bootstrap.setreg') +TAG_DCCSI_CONFIG = str('dccsiconfiguration.setreg') # filesystem markers, stub file names. -STUB_LY_DEV = str('engine.json') -STUB_LY_ROOT_PROJECT = str('ly_project_stub') -STUB_LY_ROOT_DCCSI = str('dccsi_stub') -STUB_LY_DCCSI_AZPY = str('dccsi_azpy_stub') -STUB_LY_DCCSI_SDK = str('dccsi_sdk_stub') +STUB_O3DE_DEV = str('engine.json') +STUB_O3DE_ROOT_DCCSI = str('dccsi_stub') +STUB_O3DE_DCCSI_AZPY = str('dccsi_azpy_stub') +STUB_O3DE_DCCSI_TOOLS = str('dccsi_tools_stub') # config string consts, Meta Qualifiers QUALIFIER_COMMENT = str('_meta_COMMENT') @@ -135,18 +147,18 @@ PATH_PROGRAMFILES_X64 = str(os.environ['PROGRAMFILES']) # base env var key as str ENVAR_COMPANY = str('COMPANY') -ENVAR_LY_PROJECT = str('LY_PROJECT') -ENVAR_LY_PROJECT_PATH = str('LY_PROJECT_PATH') -ENVAR_LY_DEV = str('LY_DEV') +ENVAR_O3DE_PROJECT = str('O3DE_PROJECT') +ENVAR_O3DE_PROJECT_PATH = str('O3DE_PROJECT_PATH') +ENVAR_O3DE_DEV = str('O3DE_DEV') ENVAR_DCCSIG_PATH = str('DCCSIG_PATH') ENVAR_DCCSI_AZPY_PATH = str('DCCSI_AZPY_PATH') -ENVAR_DCCSI_SDK_PATH = str('DCCSI_SDK_PATH') -ENVAR_LY_BUILD_DIR_NAME = str('LY_BUILD_DIR_NAME') +ENVAR_DCCSI_TOOLS_PATH = str('DCCSI_TOOLS_PATH') +ENVAR_O3DE_BUILD_DIR_NAME = str('O3DE_BUILD_DIR_NAME') -ENVAR_LY_BUILD_PATH = str('LY_BUILD_PATH') +ENVAR_O3DE_BUILD_PATH = str('O3DE_BUILD_PATH') ENVAR_QT_PLUGIN_PATH = TAG_QT_PLUGIN_PATH ENVAR_QTFORPYTHON_PATH = str('QTFORPYTHON_PATH') -ENVAR_LY_BIN_PATH = str('LY_BIN_PATH') +ENVAR_O3DE_BIN_PATH = str('O3DE_BIN_PATH') ENVAR_DCCSI_LOG_PATH = str('DCCSI_LOG_PATH') ENVAR_DCCSI_LAUNCHERS_PATH = str('DCCSI_LAUNCHERS_PATH') @@ -155,7 +167,7 @@ ENVAR_DCCSI_PY_VERSION_MAJOR = str('DCCSI_PY_VERSION_MAJOR') ENVAR_DCCSI_PY_VERSION_MINOR = str('DCCSI_PY_VERSION_MINOR') ENVAR_DCCSI_PYTHON_PATH = str('DCCSI_PYTHON_PATH') ENVAR_DCCSI_PYTHON_LIB_PATH = str('DCCSI_PYTHON_LIB_PATH') -ENVAR_DCCSI_PYTHON_INSTALL = str('DCCSI_PYTHON_INSTALL') +ENVAR_O3DE_PYTHON_INSTALL = str('O3DE_PYTHON_INSTALL') ENVAR_WINGHOME = str('WINGHOME') ENVAR_DCCSI_WING_VERSION_MAJOR = str('DCCSI_WING_VERSION_MAJOR') @@ -169,7 +181,7 @@ ENVAR_DCCSI_PY_DEFAULT = str('DCCSI_PY_DEFAULT') ENVAR_DCCSI_MAYA_VERSION = str('DCCSI_MAYA_VERSION') ENVAR_MAYA_LOCATION = str('MAYA_LOCATION') -ENVAR_DCCSI_SDK_MAYA_PATH = str('DCCSI_SDK_MAYA_PATH') +ENVAR_DCCSI_TOOLS_MAYA_PATH = str('DCCSI_TOOLS_MAYA_PATH') ENVAR_MAYA_MODULE_PATH = str('MAYA_MODULE_PATH') ENVAR_MAYA_BIN_PATH = str('MAYA_BIN_PATH') @@ -188,33 +200,33 @@ ENVAR_MAYA_SCRIPT_PATH = str('MAYA_SCRIPT_PATH') ENVAR_DCCSI_MAYA_SET_CALLBACKS = str('DCCSI_MAYA_SET_CALLBACKS') -TAG_LY_DCC_MAYA_MEL = 'dccsi_setup.mel' +TAG_O3DE_DCC_MAYA_MEL = 'dccsi_setup.mel' TAG_MAYA_WORKSPACE = 'workspace.mel' # dcc scripting interface common and default paths -PATH_LY_DEV = str(return_stub_dir(STUB_LY_DEV)) -PATH_DCCSIG_PATH = str(return_stub_dir(STUB_LY_ROOT_DCCSI)) -PATH_DCCSI_AZPY_PATH = str(return_stub_dir(STUB_LY_DCCSI_AZPY)) -PATH_DCCSI_SDK_PATH = str('{0}\\{1}'.format(PATH_DCCSIG_PATH, TAG_DIR_DCCSI_SDK)) +PATH_O3DE_DEV = str(return_stub_dir(STUB_O3DE_DEV)) +PATH_DCCSIG_PATH = str(return_stub_dir(STUB_O3DE_ROOT_DCCSI)) +PATH_DCCSI_AZPY_PATH = str(return_stub_dir(STUB_O3DE_DCCSI_AZPY)) +PATH_DCCSI_TOOLS_PATH = str('{0}\\{1}'.format(PATH_DCCSIG_PATH, TAG_DIR_DCCSI_TOOLS)) # logging into the cache -PATH_DCCSI_LOG_PATH = str('{LY_DEV}\\Cache\\{LY_PROJECT}\\pc\\user\\log\\logs') +PATH_DCCSI_LOG_PATH = str('{O3DE_PROJECT_PATH}\\user\\log\{TAG_DCCSI_NICKNAME}') # dev \ \ -STR_CONSTRUCT_LY_BUILD_PATH = str('{0}\\{1}') -PATH_LY_BUILD_PATH = str(STR_CONSTRUCT_LY_BUILD_PATH.format(PATH_LY_DEV, - TAG_DIR_LY_BUILD)) +STR_CONSTRUCT_O3DE_BUILD_PATH = str('{0}\\{1}') +PATH_O3DE_BUILD_PATH = str(STR_CONSTRUCT_O3DE_BUILD_PATH.format(PATH_O3DE_DEV, + TAG_DIR_O3DE_BUILD_FOLDER)) # ENVAR_QT_PLUGIN_PATH = TAG_QT_PLUGIN_PATH STR_QTPLUGIN_DIR = str('{0}\\bin\\profile\\EditorPlugins') STR_QTFORPYTHON_PATH = str('{0}\\Gems\\QtForPython\\3rdParty\\pyside2\\windows\\release') -STR_LY_BIN_PATH = str('{0}\\bin\\profile') +STR_O3DE_BIN_PATH = str('{0}\\bin\\profile') -PATH_LY_BUILD_PATH = str('{0}\\{1}'.format(PATH_LY_DEV, TAG_DIR_LY_BUILD)) -PATH_QTFORPYTHON_PATH = str(STR_QTFORPYTHON_PATH.format(PATH_LY_DEV)) -PATH_QT_PLUGIN_PATH = str(STR_QTPLUGIN_DIR).format(PATH_LY_BUILD_PATH) -PATH_LY_BIN_PATH = str(STR_LY_BIN_PATH).format(PATH_LY_BUILD_PATH) +PATH_O3DE_BUILD_PATH = str('{0}\\{1}'.format(PATH_O3DE_DEV, TAG_DIR_O3DE_BUILD_FOLDER)) +PATH_QTFORPYTHON_PATH = str(STR_QTFORPYTHON_PATH.format(PATH_O3DE_DEV)) +PATH_QT_PLUGIN_PATH = str(STR_QTPLUGIN_DIR).format(PATH_O3DE_BUILD_PATH) +PATH_O3DE_BIN_PATH = str(STR_O3DE_BIN_PATH).format(PATH_O3DE_BUILD_PATH) # py path string, parts, etc. TAG_DEFAULT_PY = str('Launch_pyBASE.bat') @@ -233,12 +245,19 @@ parts = os.path.split(PATH_USER_HOME) if str(parts[1].lower()) == 'documents': PATH_USER_HOME = parts[0] _LOGGER.debug('user home CORRECTED: {}'.format(PATH_USER_HOME)) + +STR_USER_O3DE_PATH = str('{home}\\{o3de}') -PATH_USER_O3DE = str('{home}\\{o3de}').format(home=PATH_USER_HOME, +PATH_USER_O3DE = str(STR_USER_O3DE_PATH).format(home=PATH_USER_HOME, o3de=TAG_O3DE_FOLDER) -PATH_USER_O3DE_REGISTRY = str('{0}\\Registry').format(PATH_USER_O3DE) -PATH_USER_O3DE_BOOTSTRAP = str('{reg}\\{file}').format(reg=PATH_USER_O3DE_REGISTRY, - file=TAG_O3DE_BOOTSTRAP) + +TAG_DIR_REGISTRY = str('Registry') +STR_USER_O3DE_REGISTRY_PATH = str('{0}\\{1}') +PATH_USER_O3DE_REGISTRY = str(STR_USER_O3DE_REGISTRY_PATH).format(PATH_USER_O3DE, TAG_DIR_REGISTRY) + +STR_USER_O3DE_BOOTSTRAP_PATH = str('{reg}\\{file}') +PATH_USER_O3DE_BOOTSTRAP = str(STR_USER_O3DE_BOOTSTRAP_PATH).format(reg=PATH_USER_O3DE_REGISTRY, + file=TAG_O3DE_BOOTSTRAP) #python and site-dir TAG_DCCSI_PY_VERSION_MAJOR = str(3) @@ -247,8 +266,8 @@ TAG_DCCSI_PY_VERSION_RELEASE = str(10) TAG_PYTHON_EXE = str('python.exe') TAG_TOOLS_DIR = str('Tools\\Python') TAG_PLATFORM = str('windows') -STR_CONSTRUCT_DCCSI_PYTHON_INSTALL = str('{0}\\{1}\\{2}.{3}.{4}\\{5}') -PATH_DCCSI_PYTHON_PATH = str(STR_CONSTRUCT_DCCSI_PYTHON_INSTALL.format(PATH_LY_DEV, +STR_CONSTRUCT_O3DE_PYTHON_INSTALL = str('{0}\\{1}\\{2}.{3}.{4}\\{5}') +PATH_DCCSI_PYTHON_PATH = str(STR_CONSTRUCT_O3DE_PYTHON_INSTALL.format(PATH_O3DE_DEV, TAG_TOOLS_DIR, TAG_DCCSI_PY_VERSION_MAJOR, TAG_DCCSI_PY_VERSION_MINOR, @@ -290,19 +309,12 @@ PATH_SAT_INSTALL_PATH = str('{0}\\{1}\\{2}\\{3}\\{4}' # Main Code Block, runs this script as main (testing) # ------------------------------------------------------------------------- if __name__ == '__main__': - # there are not really tests to run here due to this being a list of - # constants for shared use. - _G_DEBUG = True - _DCCSI_DEV_MODE = True - _LOGGER.setLevel(_logging.DEBUG) # force debugging - - # this is a top level module and to reduce cyclical azpy imports - # it only has a basic logger configured, add log to console - _handler = _logging.StreamHandler(sys.stdout) - _handler.setLevel(_logging.DEBUG) - _formatter = _logging.Formatter(FRMT_LOG_LONG) - _handler.setFormatter(_formatter) - _LOGGER.addHandler(_handler) + """Run this file as a standalone script""" + + # overide logger for standalone to be more verbose and lof to file + _LOGGER = azpy.initialize_logger(_MODULENAME, + log_to_file=_DCCSI_GDEBUG, + default_log_level=_DCCSI_LOGLEVEL) # happy print _LOGGER.info(STR_CROSSBAR) @@ -321,15 +333,15 @@ if __name__ == '__main__': from pathlib import Path _stash_dict = {} - _stash_dict['LY_DEV'] = Path(PATH_LY_DEV) + _stash_dict['O3DE_DEV'] = Path(PATH_O3DE_DEV) _stash_dict['DCCSIG_PATH'] = Path(PATH_DCCSIG_PATH) _stash_dict['DCCSI_AZPY_PATH'] = Path(PATH_DCCSI_AZPY_PATH) - _stash_dict['DCCSI_SDK_PATH'] = Path(PATH_DCCSI_SDK_PATH) + _stash_dict['DCCSI_TOOLS_PATH'] = Path(PATH_DCCSI_TOOLS_PATH) _stash_dict['DCCSI_PYTHON_PATH'] = Path(PATH_DCCSI_PYTHON_PATH) _stash_dict['DCCSI_PY_BASE'] = Path(PATH_DCCSI_PY_BASE) _stash_dict['DCCSI_PYTHON_LIB_PATH'] = Path(PATH_DCCSI_PYTHON_LIB_PATH) - _stash_dict['LY_BUILD_PATH'] = Path(PATH_LY_BUILD_PATH) - _stash_dict['LY_BIN_PATH'] = Path(PATH_LY_BIN_PATH) + _stash_dict['O3DE_BUILD_PATH'] = Path(PATH_O3DE_BUILD_PATH) + _stash_dict['O3DE_BIN_PATH'] = Path(PATH_O3DE_BIN_PATH) _stash_dict['QTFORPYTHON_PATH'] = Path(PATH_QTFORPYTHON_PATH) _stash_dict['QT_PLUGIN_PATH'] = Path(PATH_QT_PLUGIN_PATH) _stash_dict['SAT_INSTALL_PATH'] = Path(PATH_SAT_INSTALL_PATH) @@ -339,7 +351,7 @@ if __name__ == '__main__': # py 2 and 3 compatible iter def get_items(dict_object): for key in dict_object: - yield key, dict_object[key] + yield key, dict_object[key] for key, value in get_items(_stash_dict): # check if path exists diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/__init__.py new file mode 100644 index 0000000000..9269739f96 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/__init__.py @@ -0,0 +1,42 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- +__copyright__ = "Copyright 2021, Amazon" +# ------------------------------------------------------------------------- +import sys +import logging as _logging +# ------------------------------------------------------------------------- + +#pulling from azpy.constants causes cyclical imports :( +FRMT_LOG_LONG = "[%(name)s][%(levelname)s] >> %(message)s (%(asctime)s; %(filename)s:%(lineno)d)" +_DCCSI_GDEBUG = False +_DCCSI_LOGLEVEL = int(20) +if _DCCSI_GDEBUG: + _DCCSI_LOGLEVEL = int(10) + +_PACKAGENAME = 'azpy.core' +_logging.basicConfig(format=FRMT_LOG_LONG, level=_DCCSI_LOGLEVEL) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) + +__all__ = [] + +if sys.version_info >= (3, 6): + from azpy.core.py3.utils import get_datadir +elif sys.version_info >= (2, 6) and sys.version_info < (3, 6): + _LOGGER.warning('Python vesion < 3 will be deprecated in the future') + from azpy.core.py2.utils import get_datadir +else: + _LOGGER.warning('Python vesion < 2.6 not recommended') + from azpy.core.py2.utils import get_datadir + +__all__ = ['get_datadir'] # you should hope it works + +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py2/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py2/__init__.py new file mode 100644 index 0000000000..35727ddda6 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py2/__init__.py @@ -0,0 +1,20 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- +__copyright__ = "Copyright 2021, Amazon" +# ------------------------------------------------------------------------- +import logging as _logging +# ------------------------------------------------------------------------- +_PACKAGENAME = 'azpy.core.py2' +_LOGGER = _logging.getLogger(_PACKAGENAME) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) + +__all__ = [] +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py2/utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py2/utils.py new file mode 100644 index 0000000000..3a79b1bccf --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py2/utils.py @@ -0,0 +1,59 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- +"""DCCsi.azpy.core.py2.utils +This module contain versions of utils speciufic to py3 snyntax""" +import sys +import logging as _logging + +try: + import pathlib +except: + import pathlib2 as pathlib + +__all__ = ['get_datadir'] + +_MODULENAME = 'azpy.core.py2.utils' +_LOGGER = _logging.getLogger(_MODULENAME) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) + +# ------------------------------------------------------------------------- +def get_datadir(): + """ + persistent application data. + # linux: ~/.local/share + # macOS: ~/Library/Application Support + # windows: C:/Users//AppData/Roaming + """ + + home = pathlib.Path.home() + + if sys.platform.startswith('win'): + datadir = pathlib.Path(home, "AppData/Roaming") + return datadir + elif sys.platform == "linux": + datadir = pathlib.Path(home, ".local/share") + return datadir + elif sys.platform == "darwin": + datadir = pathlib.Path(home, "Library/Application Support") + return datadir + else: # unknown + return None +# ------------------------------------------------------------------------- + + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + """module testing""" + + user_data_dir = get_datadir() + _LOGGER.info(user_data_dir.resolve()) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py3/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py3/__init__.py new file mode 100644 index 0000000000..f5d26c9ebe --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py3/__init__.py @@ -0,0 +1,20 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- +__copyright__ = "Copyright 2021, Amazon" +# ------------------------------------------------------------------------- +import logging as _logging +# ------------------------------------------------------------------------- +_PACKAGENAME = 'azpy.core.py3' +_LOGGER = _logging.getLogger(_PACKAGENAME) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) + +__all__ = [] +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py3/utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py3/utils.py new file mode 100644 index 0000000000..cca07eb7dc --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py3/utils.py @@ -0,0 +1,52 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- +"""DCCsi.azpy.core.py3.utils +This module contain versions of utils speciufic to py3 snyntax""" +import sys +import pathlib +import logging as _logging + +__all__ = ['get_datadir'] + +_MODULENAME = 'azpy.core.py3.utils' +_LOGGER = _logging.getLogger(_MODULENAME) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) + +# ------------------------------------------------------------------------- +def get_datadir() -> pathlib.Path: + """ + persistent application data. + # linux: ~/.local/share + # macOS: ~/Library/Application Support + # windows: C:/Users//AppData/Roaming + """ + + home = pathlib.Path.home() + + if sys.platform.startswith('win'): + return home / "AppData/Roaming" + elif sys.platform == "linux": + return home / ".local/share" + elif sys.platform == "darwin": + return home / "Library/Application Support" + else: # unknown + return None +# ------------------------------------------------------------------------- + + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + """module testing""" + + user_data_dir = get_datadir() + _LOGGER.info(user_data_dir.resolve()) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/3dsmax/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/3dsmax/__init__.py old mode 100755 new mode 100644 similarity index 71% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/3dsmax/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/3dsmax/__init__.py index 017730d63c..a98de89ad7 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/3dsmax/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/3dsmax/__init__.py @@ -12,38 +12,33 @@ # importing all of the modules """azpy.3dsmax.__init__""" -import os -from azpy.env_bool import env_bool +import logging as _logging + +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.3dsmax' + _PACKAGENAME = 'azpy.dcc.3dsmax' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- - +# These are explicit imports for now __all__ = [] - -# ------------------------------------------------------------------------- - -# ------------------------------------------------------------------------- -if _DCCSI_DEV_MODE: - # If in dev mode this will test imports of __all__ - from azpy import test_imports - _LOGGER.debug('Testing Imports from {0}'.format(_PACKAGENAME)) - test_imports(__all__, - _pkg=_PACKAGENAME, - _logger=_LOGGER) +# To Do: procedurally discover dcc access and extend __all__ # ------------------------------------------------------------------------- @@ -53,14 +48,26 @@ def init(): then it should be initialized and added here so general imports don't fail""" - # __all__.append() - # Make sure we can import the native apis import pymxs import MaxPlus + + # extend all with submodules + #__all__.append('foo', 'bar') # Importing local packages/modules pass # ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +if _DCCSI_DEV_MODE: + # If in dev mode this will test imports of __all__ + from azpy import test_imports + _LOGGER.debug('Testing Imports from {0}'.format(_PACKAGENAME)) + test_imports(__all__, + _pkg=_PACKAGENAME, + _logger=_LOGGER) +# ------------------------------------------------------------------------- + del _LOGGER diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/__init__.py new file mode 100644 index 0000000000..7f2b402c95 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/__init__.py @@ -0,0 +1,53 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- + +"""azpy.shared.__init__""" + +import logging as _logging + +import azpy.env_bool as env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG +from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG + +# global space +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) + +_PACKAGENAME = __name__ +if _PACKAGENAME is '__main__': + _PACKAGENAME = 'azpy.dcc' + +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) + +# ------------------------------------------------------------------------- +# These are explicit imports for now +__all__ = [] +# To Do: procedurally discover dcc access and extend __all__ +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +if _DCCSI_DEV_MODE: + # If in dev mode this will test imports of __all__ + from azpy import test_imports + _LOGGER.debug('Testing Imports from {0}'.format(_PACKAGENAME)) + test_imports(__all__, + _pkg=_PACKAGENAME, + _logger=_LOGGER) +# ------------------------------------------------------------------------- + +del _LOGGER diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/blender/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/blender/__init__.py old mode 100755 new mode 100644 similarity index 71% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/blender/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/blender/__init__.py index 903b76e3c4..b46c7146c2 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/blender/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/blender/__init__.py @@ -12,39 +12,32 @@ # importing all of the modules """azpy.blender.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.blender' - -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -#_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) + _PACKAGENAME = 'azpy.dcc.blender' -# ------------------------------------------------------------------------- - -__all__ = [] +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- - - -# ------------------------------------------------------------------------- -if _DCCSI_DEV_MODE: - # If in dev mode this will test imports of __all__ - from azpy import test_imports - _LOGGER.debug('Testing Imports from {0}'.format(_PACKAGENAME)) - test_imports(__all__, - _pkg=_PACKAGENAME, - _logger=_LOGGER) +# These are explicit imports for now +__all__ = [] +# To Do: procedurally discover dcc access and extend __all__ # ------------------------------------------------------------------------- @@ -53,14 +46,26 @@ def init(): """If the blender bpy api is required for a package/module to import, then it should be initialized and added here so general imports don't fail""" - - # __all__.append() # Make sure we can import the native apis import bpy + + # extend all with submodules + #__all__.append('foo', 'bar') # Importing local packages/modules pass # ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +if _DCCSI_DEV_MODE: + # If in dev mode this will test imports of __all__ + from azpy import test_imports + _LOGGER.debug('Testing Imports from {0}'.format(_PACKAGENAME)) + test_imports(__all__, + _pkg=_PACKAGENAME, + _logger=_LOGGER) +# ------------------------------------------------------------------------- + del _LOGGER diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/houdini/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/houdini/__init__.py old mode 100755 new mode 100644 similarity index 70% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/houdini/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/houdini/__init__.py index c0b1ced437..0c16872073 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/houdini/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/houdini/__init__.py @@ -12,28 +12,32 @@ # importing all of the modules """azpy.houdini.__init__""" -import os +import logging as _logging -from azpy import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.houdini' + _PACKAGENAME = 'azpy.dcc.houdini' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -#_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- - +# These are explicit imports for now __all__ = [] - +# To Do: procedurally discover dcc access and extend __all__ # ------------------------------------------------------------------------- @@ -53,12 +57,13 @@ def init(): """If the houdini api is required for a package/module to import, then it should be initialized and added here so general imports don't fail""" - - # __all__.append() # Make sure we can import the native apis import hou - + + # extend all with submodules + #__all__.append('foo', 'bar') + # Importing local packages/modules pass # ------------------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/marmoset/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/marmoset/__init__.py old mode 100755 new mode 100644 similarity index 70% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/marmoset/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/marmoset/__init__.py index 35a081dcbf..7e95b5f19e --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/marmoset/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/marmoset/__init__.py @@ -12,28 +12,32 @@ # importing all of the modules """azpy.houdini.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.marmoset' + _PACKAGENAME = 'azpy.dcc.marmoset' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- - +# These are explicit imports for now __all__ = [] - +# To Do: procedurally discover dcc access and extend __all__ # ------------------------------------------------------------------------- @@ -53,12 +57,13 @@ def init(): """If the marmoset api is required for a package/module to import, then it should be initialized and added here so general imports don't fail""" - - # __all__.append() # Make sure we can import the native apis import mset - + + # extend all with submodules + #__all__.append('foo', 'bar') + # Importing local packages/modules pass # ------------------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/__init__.py old mode 100755 new mode 100644 similarity index 64% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/__init__.py index e66ee63739..870ace6166 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/__init__.py @@ -10,40 +10,32 @@ # -- This line is 75 characters ------------------------------------------- # The __init__.py files help guide import statements without automatically # importing all of the modules -"""azpy.maya.__init__""" +"""azpy.dcc.maya.__init__""" -from azpy.env_bool import env_bool +import logging as _logging + +import azpy.env_bool as env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya' - -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) + _PACKAGENAME = 'azpy.dcc.maya' -# ------------------------------------------------------------------------- +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) __all__ = [] -# ------------------------------------------------------------------------- - - -# ------------------------------------------------------------------------- -if _DCCSI_DEV_MODE: - # If in dev mode this will test imports of __all__ - from azpy import test_imports - _LOGGER.debug('Testing Imports from {0}'.format(_PACKAGENAME)) - test_imports(__all__, - _pkg=_PACKAGENAME, - _logger=_LOGGER) -# ------------------------------------------------------------------------- - - # ------------------------------------------------------------------------- def init(): """If the maya api is required for a package/module to import, diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/__init__.py old mode 100755 new mode 100644 similarity index 64% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/__init__.py index 3f36bb0fba..7de93fe2bb --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/__init__.py @@ -12,23 +12,26 @@ # importing all of the modules """azpy.maya.callbacks.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': _PACKAGENAME = 'azpy.maya.callbacks' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) __all__ = ['event_callback_handler', 'node_message_callback_handler', diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/event_callback_handler.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/event_callback_handler.py old mode 100755 new mode 100644 similarity index 98% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/event_callback_handler.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/event_callback_handler.py index 7603e969e3..d2ca596d3c --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/event_callback_handler.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/event_callback_handler.py @@ -73,12 +73,12 @@ import maya.api.OpenMaya as openmaya #-------------------------------------------------------------------------- # -- Misc Global Space Definitions -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.callbacks.event_callback_handler' + _PACKAGENAME = 'azpy.dcc.maya.callbacks.event_callback_handler' _LOGGER = azpy.initialize_logger(_PACKAGENAME, default_log_level=int(20)) _LOGGER.debug('Invoking:: {0}.'.format({_PACKAGENAME})) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/node_message_callback_handler.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/node_message_callback_handler.py old mode 100755 new mode 100644 similarity index 98% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/node_message_callback_handler.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/node_message_callback_handler.py index cfb883be18..e41c9dbbc7 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/node_message_callback_handler.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/node_message_callback_handler.py @@ -80,12 +80,12 @@ import maya.cmds as mc # ------------------------------------------------------------------------- # -- Misc Global Space Definitions -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.callbacks.event_callback_handler' + _PACKAGENAME = 'azpy.dcc.maya.callbacks.event_callback_handler' _LOGGER = azpy.initialize_logger(_PACKAGENAME, default_log_level=int(20)) _LOGGER.debug('Invoking:: {0}.'.format({_PACKAGENAME})) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/on_shader_rename.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/on_shader_rename.py old mode 100755 new mode 100644 similarity index 98% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/on_shader_rename.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/on_shader_rename.py index 84e2426c2b..834505f076 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/on_shader_rename.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/on_shader_rename.py @@ -85,12 +85,12 @@ import maya.cmds as mc # -------------------------------------------------------------------------- # -- Misc Global Space Definitions -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.callbacks.on_shader_rename' + _PACKAGENAME = 'azpy.dcc.maya.callbacks.on_shader_rename' _LOGGER = azpy.initialize_logger(_PACKAGENAME, default_log_level=int(20)) _LOGGER.debug('Invoking:: {0}.'.format({_PACKAGENAME})) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/__init__.py old mode 100755 new mode 100644 similarity index 55% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/__init__.py index 7dab357d22..ecb164c1f7 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/__init__.py @@ -10,25 +10,28 @@ # -- This line is 75 characters ------------------------------------------- # The __init__.py files help guide import statements without automatically # importing all of the modules -"""azpy.maya.helpers.__init__""" +"""azpy.dcc.maya.helpers.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.callbacks' + _PACKAGENAME = 'azpy.dcc.maya.callbacks' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) __all__ = ['undo_context', 'utils'] diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/undo_context.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/undo_context.py old mode 100755 new mode 100644 similarity index 97% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/undo_context.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/undo_context.py index 90c12af2c9..7a6de2224b --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/undo_context.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/undo_context.py @@ -42,12 +42,12 @@ import maya.cmds as mc # ------------------------------------------------------------------------- # -- Misc Global Space Definitions -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.helpers.undo_context' + _PACKAGENAME = 'azpy.dcc.maya.helpers.undo_context' _LOGGER = initialize_logger(_PACKAGENAME, default_log_level=int(20)) _LOGGER.debug('Invoking:: {0}.'.format({_PACKAGENAME})) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/utils.py old mode 100755 new mode 100644 similarity index 98% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/utils.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/utils.py index 5fe6cd5876..5f1522e6e1 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/utils.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/utils.py @@ -10,7 +10,7 @@ # -- This line is 75 characters ------------------------------------------- """ -azpy.maya utility module +azpy.dcc.maya utility module """ # ------------------------------------------------------------------------- # built in's @@ -32,12 +32,12 @@ import maya.cmds as cmds # ------------------------------------------------------------------------- # -- Misc Global Space Definitions -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.helpers.undo_context' + _PACKAGENAME = 'azpy.dcc.maya.helpers.undo_context' _LOGGER = initialize_logger(_PACKAGENAME, default_log_level=int(20)) _LOGGER.debug('Invoking:: {0}.'.format({_PACKAGENAME})) @@ -46,7 +46,7 @@ _LOGGER.debug('Invoking:: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- # Initiate the Wing IDE debug connection. -if _G_DEBUG: +if _DCCSI_GDEBUG: #import azpy.dev.connectDebugger as lyDevConnnect # lyDevConnnect() pass diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/toolbits/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/toolbits/__init__.py new file mode 100644 index 0000000000..41f177bcf0 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/toolbits/__init__.py @@ -0,0 +1,38 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# -------------------------------------------------------------------------- +"""azpy.dcc.maya.toolbits.__init__""" + +import logging as _logging + +import azpy.env_bool as env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG +from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG + +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) + +_PACKAGENAME = __name__ +if _PACKAGENAME is '__main__': + _PACKAGENAME = 'azpy.dcc.maya.toolbits' + +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) + +__all__ = ['detach'] + +del _LOGGER +#-------------------------------------------------------------------------- + diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/toolbits/detach.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/toolbits/detach.py old mode 100755 new mode 100644 similarity index 94% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/toolbits/detach.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/toolbits/detach.py index 1afcbfa9f6..51d3bb9b66 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/toolbits/detach.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/toolbits/detach.py @@ -44,12 +44,12 @@ from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.toolbits.detatch' + _PACKAGENAME = 'azpy.dcc.maya.toolbits.detatch' import azpy _LOGGER = azpy.initialize_logger(_PACKAGENAME) @@ -65,13 +65,13 @@ def clean_detach(detachType=0, args=None, name=None, or duplicating those faces without harming the orignal ''' - sel = azpy.maya.helpers.utils.Selection() + sel = azpy.dcc.maya.helpers.utils.Selection() for obj in sel.selection.keys(): print("~ cleanDetach:: Working on: {0}".format(obj)) # set up / open the maya undo context - with azpy.maya.helpers.UndoContext(): + with azpy.dcc.maya.helpers.UndoContext(): if deletHistoyIn: mc.delete( obj, constructionHistory = True) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/__init__.py similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/__init__.py diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/execute_wing_code.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/execute_wing_code.py similarity index 95% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/execute_wing_code.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/execute_wing_code.py index 34aaa2cbb7..3c8e7a75a3 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/execute_wing_code.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/execute_wing_code.py @@ -43,13 +43,13 @@ def get_stub_check_path(in_path=__file__, check_stub='engineroot.txt'): # ------------------------------------------------------------------------- # -- Global Definitions -- -_MODULENAME = 'azpy.maya.utils.execute_wing_code' +_MODULENAME = 'azpy.dcc.maya.utils.execute_wing_code' _LOGGER = _logging.getLogger(_MODULENAME) -_LY_DEV = get_stub_check_path() -_LOGGER.info('_LY_DEV: {}'.format(_LY_DEV)) +_O3DE_DEV = get_stub_check_path() +_LOGGER.info('_O3DE_DEV: {}'.format(_O3DE_DEV)) -_PROJ_CACHE = os.path.join(_LY_DEV, 'cache', 'DCCsi', 'wing') +_PROJ_CACHE = os.path.join(_O3DE_DEV, 'cache', 'DCCsi', 'wing') _LOGGER.info('_PROJ_CACHE: {}'.format(_PROJ_CACHE)) _LOCAL_HOST = socket.gethostbyname(socket.gethostname()) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/simple_command_port.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/simple_command_port.py similarity index 98% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/simple_command_port.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/simple_command_port.py index 659cae985d..3a130cd85f 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/simple_command_port.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/simple_command_port.py @@ -24,7 +24,7 @@ import logging as _logging # -------------------------------------------------------------------------- # -- Global Definitions -- -_MODULENAME = 'azpy.maya.utils.simple_command_port' +_MODULENAME = 'azpy.dcc.maya.utils.simple_command_port' _LOGGER = _logging.getLogger(_MODULENAME) _LOCAL_HOST = socket.gethostbyname(socket.gethostname()) @@ -205,7 +205,7 @@ class SimpleCommandPort: # Main Code Block, runs this script as main (testing) # ------------------------------------------------------------------------- if __name__ == '__main__': - _G_DEBUG = True + _DCCSI_GDEBUG = True _DCCSI_DEV_MODE = True _LOGGER.setLevel(_logging.DEBUG) # force debugging diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/wing_to_maya.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/wing_to_maya.py similarity index 99% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/wing_to_maya.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/wing_to_maya.py index f170090823..0c06b912d9 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/wing_to_maya.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/wing_to_maya.py @@ -24,7 +24,7 @@ from simple_command_port import SimpleCommandPort # -------------------------------------------------------------------------- # -- Global Definitions -- -_MODULENAME = 'azpy.maya.utils.wing_to_maya' +_MODULENAME = 'azpy.dcc.maya.utils.wing_to_maya' _LOGGER = _logging.getLogger(_MODULENAME) _LOCAL_HOST = socket.gethostbyname(socket.gethostname()) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/lumberyard/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/o3de/__init__.py old mode 100755 new mode 100644 similarity index 68% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/lumberyard/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/o3de/__init__.py index 9da4e8975a..95aa781557 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/lumberyard/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/o3de/__init__.py @@ -11,31 +11,51 @@ # The __init__.py files help guide import statements without automatically # importing all of the modules """azpy.lumberyard.__init__ -All Lumberyard render related packages/modules should live here.""" +All O3DE related extension packages/modules should live here.""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.lumberyard' + _PACKAGENAME = 'azpy.dcc.o3de' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- - +# These are explicit imports for now __all__ = [] +# To Do: procedurally discover dcc access and extend __all__ +# ------------------------------------------------------------------------- + # ------------------------------------------------------------------------- +def init(): + """If the lumberyard azlmbr api is required for a package/module to + import, then it should be initialized and added here so general imports + don't fail""" + + import azlmbr + + # extend all with submodules + __all__.append('atom') + + # Importing local packages/modules + pass +# ------------------------------------------------------------------------- # ------------------------------------------------------------------------- @@ -49,19 +69,4 @@ if _DCCSI_DEV_MODE: # ------------------------------------------------------------------------- -# ------------------------------------------------------------------------- -def init(): - """If the lumberyard azlmbr api is required for a package/module to - import, then it should be initialized and added here so general imports - don't fail""" - - # __all__.append() - - # Make sure we can import the native apis - #import - - # Importing local packages/modules - pass -# ------------------------------------------------------------------------- - del _LOGGER diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/render/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/o3de/atom/__init__.py old mode 100755 new mode 100644 similarity index 72% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/render/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/o3de/atom/__init__.py index a6eb9ea373..ecd1db3ef0 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/render/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/o3de/atom/__init__.py @@ -14,28 +14,33 @@ This package generically uses 'render' to refer to Atom (which is a code name.) All Atom render related packages/modules should live here.""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) + _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.render' - + _PACKAGENAME = 'azpy.dcc.o3de.atom' -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- - +# These are explicit imports for now __all__ = [] - +# To Do: procedurally discover dcc access and extend __all__ # ------------------------------------------------------------------------- @@ -55,12 +60,13 @@ def init(): """If the atom render api is required for a package/module to import, then it should be initialized and added here so general imports don't fail""" - - # __all__.append() # Make sure we can import the native apis # import - + + # extend all with submodules + #__all__.append('foo', 'bar') + # Importing local packages/modules pass # ------------------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/substance/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/substance/__init__.py old mode 100755 new mode 100644 similarity index 71% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/substance/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/substance/__init__.py index 8f9d542906..1e5ee058cc --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/substance/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/substance/__init__.py @@ -12,39 +12,32 @@ # importing all of the modules """azpy.substance.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.substance' + _PACKAGENAME = 'azpy.dcc.substance' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- - +# These are explicit imports for now __all__ = [] - -# ------------------------------------------------------------------------- - - -# ------------------------------------------------------------------------- -if _DCCSI_DEV_MODE: - # If in dev mode this will test imports of __all__ - from azpy import test_imports - _LOGGER.debug('Testing Imports from {0}'.format(_PACKAGENAME)) - test_imports(__all__, - _pkg=_PACKAGENAME, - _logger=_LOGGER) +# To Do: procedurally discover dcc access and extend __all__ # ------------------------------------------------------------------------- @@ -53,15 +46,26 @@ def init(): """If the substance api is required for a package/module to import, then it should be initialized and added here so general imports don't fail""" - - # __all__.append() # Make sure we can import the native apis # import - + + # extend all with submodules + #__all__.append('foo', 'bar') + # Importing local packages/modules pass +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +if _DCCSI_DEV_MODE: + # If in dev mode this will test imports of __all__ + from azpy import test_imports + _LOGGER.debug('Testing Imports from {0}'.format(_PACKAGENAME)) + test_imports(__all__, + _pkg=_PACKAGENAME, + _logger=_LOGGER) # ------------------------------------------------------------------------- del _LOGGER diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/__init__.py index cbd58ba5e8..0db99ff838 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/__init__.py @@ -15,7 +15,7 @@ from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = 'azpy.dev.ide' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/hot_keys.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/hot_keys.py index 2c91398b70..1e179003fe 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/hot_keys.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/hot_keys.py @@ -119,10 +119,10 @@ def get_stub_check_path(in_path=__file__, check_stub='engineroot.txt'): # ------------------------------------------------------------------------- # globals -_LY_DEV = get_stub_check_path() -_LOGGER.info('_LY_DEV: {}'.format(_LY_DEV)) +_O3DE_DEV = get_stub_check_path() +_LOGGER.info('_O3DE_DEV: {}'.format(_O3DE_DEV)) -_PROJ_CACHE = os.path.join(_LY_DEV, 'cache', 'DCCsi', 'wing') +_PROJ_CACHE = os.path.join(_O3DE_DEV, 'cache', 'DCCsi', 'wing') _LOGGER.info('_PROJ_CACHE: {}'.format(_PROJ_CACHE)) if not os.path.exists(_PROJ_CACHE): @@ -339,7 +339,7 @@ mel_selection_to_maya.contexts = [ if __name__ == '__main__': # there are not really tests to run here due to this being a list of # constants for shared use. - _G_DEBUG = True + _DCCSI_GDEBUG = True _DCCSI_DEV_MODE = True _LOGGER.setLevel(_logging.DEBUG) # force debugging diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/test.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/test.py index dbf5025f88..838e890184 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/test.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/test.py @@ -73,7 +73,7 @@ dccsi_test_script.contexts = [ if __name__ == '__main__': # there are not really tests to run here due to this being a list of # constants for shared use. - _G_DEBUG = True + _DCCSI_GDEBUG = True _DCCSI_DEV_MODE = True _LOGGER.setLevel(_logging.DEBUG) # force debugging diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/maya_app.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/maya_app.py index 76dc3d9b2a..3e4344aecc 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/maya_app.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/maya_app.py @@ -25,10 +25,10 @@ from azpy.constants import ENVAR_DCCSI_DEV_MODE # -------------------------------------------------------------------------- # -- Global Definitions -- -_DCCSI_DCC_APP = None +_DCCSI_G_DCC_APP = None # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _MODULENAME = 'azpy.dev.utils.check.maya_app' @@ -43,14 +43,14 @@ _LOGGER = _logging.getLogger(_MODULENAME) def set_dcc_app(dcc_app='maya'): """ azpy.dev.utils.check.maya.set_dcc_app() - this will set global _DCCSI_DCC_APP = 'maya' - and os.environ["DCCSI_DCC_APP"] = 'maya' + this will set global _DCCSI_G_DCC_APP = 'maya' + and os.environ["DCCSI_G_DCC_APP"] = 'maya' """ - _DCCSI_DCC_APP = dcc_app + _DCCSI_G_DCC_APP = dcc_app - _LOGGER.info('Setting DCCSI_DCC_APP to: {0}'.format(dcc_app)) + _LOGGER.info('Setting DCCSI_G_DCC_APP to: {0}'.format(dcc_app)) - return _DCCSI_DCC_APP + return _DCCSI_G_DCC_APP # ------------------------------------------------------------------------- @@ -58,44 +58,44 @@ def set_dcc_app(dcc_app='maya'): def clear_dcc_app(dcc_app=False): """ azpy.dev.utils.check.maya.set_dcc_app() - this will set global _DCCSI_DCC_APP = False - and os.environ["DCCSI_DCC_APP"] = False + this will set global _DCCSI_G_DCC_APP = False + and os.environ["DCCSI_G_DCC_APP"] = False """ - _DCCSI_DCC_APP = dcc_app + _DCCSI_G_DCC_APP = dcc_app - _LOGGER.info('Setting DCCSI_DCC_APP to: {0}'.format(dcc_app)) + _LOGGER.info('Setting DCCSI_G_DCC_APP to: {0}'.format(dcc_app)) - return _DCCSI_DCC_APP + return _DCCSI_G_DCC_APP # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -def validate_state(DCCSI_DCC_APP=_DCCSI_DCC_APP): +def validate_state(DCCSI_G_DCC_APP=_DCCSI_G_DCC_APP): ''' This will detect if we are running in Maya or not, then will call either, set_dcc_app('maya') or clear_dcc_app(dcc_app=False) ''' - if _G_DEBUG: + if _DCCSI_GDEBUG: _LOGGER.debug(autolog()) try: import maya.cmds as cmds - DCCSI_DCC_APP = set_dcc_app('maya') + DCCSI_G_DCC_APP = set_dcc_app('maya') except ImportError as e: _LOGGER.warning('Can not perform: import maya.cmds as cmds') - DCCSI_DCC_APP = clear_dcc_app() + DCCSI_G_DCC_APP = clear_dcc_app() else: try: if cmds.about(batch=True): - DCCSI_DCC_APP = set_dcc_app('maya') + DCCSI_G_DCC_APP = set_dcc_app('maya') except AttributeError as e: _LOGGER.warning("maya.cmds module isn't fully loaded/populated, " "(cmds populates only in batch, maya.standalone, or maya GUI)") # NO Maya - DCCSI_DCC_APP=clear_dcc_app() + DCCSI_G_DCC_APP=clear_dcc_app() - return DCCSI_DCC_APP + return DCCSI_G_DCC_APP # ------------------------------------------------------------------------- @@ -120,7 +120,7 @@ def autolog(): # ------------------------------------------------------------------------- # run the check on import -_DCCSI_DCC_APP = validate_state() +_DCCSI_G_DCC_APP = validate_state() # ------------------------------------------------------------------------- @@ -130,7 +130,7 @@ _DCCSI_DCC_APP = validate_state() if __name__ == '__main__': # there are not really tests to run here due to this being a list of # constants for shared use. - _G_DEBUG = True + _DCCSI_GDEBUG = True _DCCSI_DEV_MODE = True _LOGGER.setLevel(_logging.DEBUG) # force debugging @@ -150,5 +150,5 @@ if __name__ == '__main__': _LOGGER.info('{} ... Running script as __main__'.format(_MODULENAME)) _LOGGER.info(STR_CROSSBAR) - _DCCSI_DCC_APP = validate_state() - _LOGGER.info('Is Maya Running? _DCCSI_DCC_APP = {}'.format(_DCCSI_DCC_APP)) + _DCCSI_G_DCC_APP = validate_state() + _LOGGER.info('Is Maya Running? _DCCSI_G_DCC_APP = {}'.format(_DCCSI_G_DCC_APP)) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/running_state.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/running_state.py index 301d49a0ea..e5304cf34c 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/running_state.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/running_state.py @@ -21,10 +21,11 @@ import logging as _logging # -------------------------------------------------------------------------- # -- Global Definitions -- -_DCCSI_DCC_APP = None +_DCCSI_G_DCC_APP = None _MODULENAME = 'azpy.dev.utils.check.running_state' _LOGGER = _logging.getLogger(_MODULENAME) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) # ------------------------------------------------------------------------- @@ -36,7 +37,7 @@ class CheckRunningState(object): """ # Class Variables - DCCSI_DCC_APP = None + DCCSI_G_DCC_APP = None def __init__(self, *args, **kwargs): ''' @@ -82,32 +83,35 @@ class CheckRunningState(object): def check_known(self): # -- init -- # first let's check if any of these DCC apps are running + + # To Do?: Add O3DE checks, treat as a DCC app? + # 0 - maya first - CheckRunningState.DCCSI_DCC_APP = self.maya_running() + CheckRunningState.DCCSI_G_DCC_APP = self.maya_running() # 1 - then max - if not CheckRunningState.DCCSI_DCC_APP: - CheckRunningState.DCCSI_DCC_APP = self.max_running() + if not CheckRunningState.DCCSI_G_DCC_APP: + CheckRunningState.DCCSI_G_DCC_APP = self.max_running() else: - _LOGGER.warning('DCCSI_DCC_APP is already set: {}'.format(CheckRunningState.DCCSI_DCC_APP)) + _LOGGER.warning('DCCSI_G_DCC_APP is already set: {}'.format(CheckRunningState.DCCSI_G_DCC_APP)) # 2 - then blender - if not CheckRunningState.DCCSI_DCC_APP: - CheckRunningState.DCCSI_DCC_APP = self.blender_running() + if not CheckRunningState.DCCSI_G_DCC_APP: + CheckRunningState.DCCSI_G_DCC_APP = self.blender_running() else: - _LOGGER.warning('DCCSI_DCC_APP is already set: {}'.format(CheckRunningState.DCCSI_DCC_APP)) + _LOGGER.warning('DCCSI_G_DCC_APP is already set: {}'.format(CheckRunningState.DCCSI_G_DCC_APP)) # store checks for DCC info - if CheckRunningState.DCCSI_DCC_APP: + if CheckRunningState.DCCSI_G_DCC_APP: self.dcc_py = True # store check for is maya running headless - if CheckRunningState.DCCSI_DCC_APP == 'maya': + if CheckRunningState.DCCSI_G_DCC_APP == 'maya': self.maya_headless = self.is_maya_headless() # set a envar other modules can easily check - if CheckRunningState.DCCSI_DCC_APP: - os.environ['DCCSI_DCC_APP'] = CheckRunningState.DCCSI_DCC_APP + if CheckRunningState.DCCSI_G_DCC_APP: + os.environ['DCCSI_G_DCC_APP'] = CheckRunningState.DCCSI_G_DCC_APP # --------------------------------------------------------------------- @@ -164,13 +168,13 @@ class CheckRunningState(object): """< To Do: Need to document >""" try: import azpy.dev.utils.check.maya_app as check_dcc - DCCSI_DCC_APP = check_dcc.validate_state() + DCCSI_G_DCC_APP = check_dcc.validate_state() except ImportError as e: _LOGGER.info('Not Implemented: azpy.dev.utils.check.maya_app') - if DCCSI_DCC_APP: - CheckRunningState.DCCSI_DCC_APP = check_dcc.validate_state() - os.environ["DCCSI_DCC_APP"] = str(DCCSI_DCC_APP) - return CheckRunningState.DCCSI_DCC_APP + if DCCSI_G_DCC_APP: + CheckRunningState.DCCSI_G_DCC_APP = check_dcc.validate_state() + os.environ["DCCSI_G_DCC_APP"] = str(DCCSI_G_DCC_APP) + return CheckRunningState.DCCSI_G_DCC_APP #---------------------------------------------------------------------- # --method------------------------------------------------------------- @@ -200,13 +204,13 @@ class CheckRunningState(object): """ try: import azpy.dev.utils.check.max_app as check_dcc - CheckRunningState.DCCSI_DCC_APP = check_dcc.validate_state() + CheckRunningState.DCCSI_G_DCC_APP = check_dcc.validate_state() except ImportError as e: _LOGGER.info('Not Implemented: azpy.dev.utils.check.max') - if CheckRunningState.DCCSI_DCC_APP: - CheckRunningState.DCCSI_DCC_APP = check_dcc.validate_state() - os.environ["DCCSI_DCC_APP"] = str(CheckRunningState.DCCSI_DCC_APP) - return CheckRunningState.DCCSI_DCC_APP + if CheckRunningState.DCCSI_G_DCC_APP: + CheckRunningState.DCCSI_G_DCC_APP = check_dcc.validate_state() + os.environ["DCCSI_G_DCC_APP"] = str(CheckRunningState.DCCSI_G_DCC_APP) + return CheckRunningState.DCCSI_G_DCC_APP #---------------------------------------------------------------------- @@ -217,13 +221,13 @@ class CheckRunningState(object): """ try: import azpy.dev.utils.check.blender_app as check_dcc - CheckRunningState.DCCSI_DCC_APP = check_dcc.validate_state() + CheckRunningState.DCCSI_G_DCC_APP = check_dcc.validate_state() except ImportError as e: _LOGGER.info('Not Implemented: azpy.dev.utils.check.blender') - if CheckRunningState.DCCSI_DCC_APP: - CheckRunningState.DCCSI_DCC_APP = check_dcc.validate_state() - os.environ["DCCSI_DCC_APP"] = str(CheckRunningState.DCCSI_DCC_APP) - return CheckRunningState.DCCSI_DCC_APP + if CheckRunningState.DCCSI_G_DCC_APP: + CheckRunningState.DCCSI_G_DCC_APP = check_dcc.validate_state() + os.environ["DCCSI_G_DCC_APP"] = str(CheckRunningState.DCCSI_G_DCC_APP) + return CheckRunningState.DCCSI_G_DCC_APP #---------------------------------------------------------------------- @@ -231,7 +235,7 @@ class CheckRunningState(object): # Class Test #========================================================================== if __name__ == '__main__': - _G_DEBUG = True + _DCCSI_GDEBUG = True _DCCSI_DEV_MODE = True _LOGGER.setLevel(_logging.DEBUG) # force debugging @@ -251,4 +255,4 @@ if __name__ == '__main__': _LOGGER.info(STR_CROSSBAR) foo = CheckRunningState() - _LOGGER.info('DCCSI_DCC_APP: {}'.format(foo.DCCSI_DCC_APP)) + _LOGGER.info('DCCSI_G_DCC_APP: {}'.format(foo.DCCSI_G_DCC_APP)) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/env_base.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/env_base.py index dd28c98927..cdcf99d762 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/env_base.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/env_base.py @@ -10,7 +10,7 @@ # -- This line is 75 characters ------------------------------------------- from __future__ import unicode_literals ''' -Module: \azpy\shared\common\base_env.py +Module: \\azpy\\shared\\common\\base_env.py This module packs the most basic set of environment variables. @@ -57,7 +57,7 @@ _LOGGER = _logging.getLogger(_PACKAGENAME) _LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) # set up base totally non-functional defauls (denoted with $) @@ -65,15 +65,15 @@ _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _BASE_ENVVAR_DICT = OrderedDict() # project tag -_BASE_ENVVAR_DICT[ENVAR_LY_PROJECT] = '${0}'.format(ENVAR_LY_PROJECT) +_BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT] = '${0}'.format(ENVAR_O3DE_PROJECT) # paths -_BASE_ENVVAR_DICT[ENVAR_LY_DEV] = Path('${0}'.format(ENVAR_LY_DEV)) -_BASE_ENVVAR_DICT[ENVAR_LY_PROJECT_PATH] = Path('${0}'.format(ENVAR_LY_PROJECT_PATH)) +_BASE_ENVVAR_DICT[ENVAR_O3DE_DEV] = Path('${0}'.format(ENVAR_O3DE_DEV)) +_BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT_PATH] = Path('${0}'.format(ENVAR_O3DE_PROJECT_PATH)) _BASE_ENVVAR_DICT[ENVAR_DCCSIG_PATH] = Path('${0}'.format(ENVAR_DCCSIG_PATH)) _BASE_ENVVAR_DICT[ENVAR_DCCSI_LOG_PATH] = Path('${0}'.format(ENVAR_DCCSI_LOG_PATH)) _BASE_ENVVAR_DICT[ENVAR_DCCSI_AZPY_PATH] = Path('${0}'.format(ENVAR_DCCSI_AZPY_PATH)) -_BASE_ENVVAR_DICT[ENVAR_DCCSI_SDK_PATH] = Path('${0}'.format(ENVAR_DCCSI_SDK_PATH)) +_BASE_ENVVAR_DICT[ENVAR_DCCSI_TOOLS_PATH] = Path('${0}'.format(ENVAR_DCCSI_TOOLS_PATH)) # dev env flags _BASE_ENVVAR_DICT[ENVAR_DCCSI_GDEBUG] = '${0}'.format(ENVAR_DCCSI_GDEBUG) @@ -110,16 +110,16 @@ if __name__ == '__main__': # print(setEnvarDefaults(), '\r') #<-- not necissary, already called # print(BASE_ENVVAR_VALUES, '\r') _LOGGER.info('Pretty print: _BASE_ENVVAR_DICT') - print(json.dumps(_BASE_ENVVAR_DICT, - indent=4, sort_keys=False, - ensure_ascii=False), '\r') + _LOGGER.debug(json.dumps(_BASE_ENVVAR_DICT, + indent=4, sort_keys=False, + ensure_ascii=False), '\r') # retreive a Path type key from the Box - foo = _BASE_ENVVAR_DICT[ENVAR_LY_DEV] + foo = _BASE_ENVVAR_DICT[ENVAR_O3DE_DEV] _LOGGER.info('~ foo is: {0}'.format(type(foo), foo)) # simple tests - _ENV_TAG = 'LY_DEV' + _ENV_TAG = 'O3DE_DEV' foo = get_envar_default(_ENV_TAG) _LOGGER.info("~ Results of getVar on tag, '{0}':'{1}'\r".format(_ENV_TAG, foo)) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/toolbits/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/toolbits/__init__.py deleted file mode 100755 index be62dedcbe..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/toolbits/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -# coding:utf-8 -#!/usr/bin/python -# -# 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 line is 75 characters ------------------------------------------- -# The __init__.py files help guide import statements without automatically -# importing all of the modules -"""azpy.maya.toolbits.__init__""" - -mport os - -from azpy.env_bool import env_bool -from azpy.constants import ENVAR_DCCSI_GDEBUG -from azpy.constants import ENVAR_DCCSI_DEV_MODE - -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) - -_PACKAGENAME = __name__ -if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.toolbits' - -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) - -__all__ = ['detach'] - -del _LOGGER -#-------------------------------------------------------------------------- - diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/return_stub.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/return_stub.py index a5a4d40aad..478d3c355e 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/return_stub.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/return_stub.py @@ -19,7 +19,7 @@ import logging as _logging # ------------------------------------------------------------------------- # global space debug flag, no fancy stuff here we use in bootstrap -_G_DEBUG = False # manually enable to debug this file +_DCCSI_GDEBUG = False # manually enable to debug this file _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': @@ -44,7 +44,7 @@ def return_stub(stub): break if (len(tail) == 0): path = "" - if _G_DEBUG: + if _DCCSI_GDEBUG: _LOGGER.debug('~ Debug Message: I was not able to find the ' 'path to that file (stub) in a walk-up ' 'from currnet path') diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/__init__.py index 7c109f42cb..d6b6bc0d1d 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/__init__.py @@ -11,23 +11,27 @@ """azpy.shared.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': _PACKAGENAME = 'azpy.shared' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/boxDumpTest.json b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/boxDumpTest.json deleted file mode 100644 index 52c556733c..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/boxDumpTest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "ordered_box": true, - "COMPANY": "Amazon.Lumberyard", - "LY_PROJECT": "DccScriptingInterface", - "LY_DEV": "G:\\depot\\JG_PC1_spectrAtom\\dev", - "LY_BUILD_DIR_NAME": "windows_vs2019", - "LY_BUILD_PATH": "G:\\depot\\JG_PC1_spectrAtom\\dev\\windows_vs2019", - "QT_PLUGIN_PATH": "G:\\depot\\JG_PC1_spectrAtom\\dev\\bin\\profile\\EditorPlugins", - "LY_PROJECT_PATH": "G:\\depot\\JG_PC1_spectrAtom\\dev\\Gems\\AtomLyIntegration\\TechnicalArt\\DccScriptingInterface", - "DCCSIG_PATH": "G:\\depot\\JG_PC1_spectrAtom\\dev\\Gems\\AtomLyIntegration\\TechnicalArt\\DccScriptingInterface", - "DCCSI_AZPY_PATH": "G:\\depot\\JG_PC1_spectrAtom\\dev\\Gems\\AtomLyIntegration\\TechnicalArt\\DccScriptingInterface\\azpy", - "DCCSI_SDK_PATH": "G:\\depot\\JG_PC1_spectrAtom\\dev\\Gems\\AtomLyIntegration\\TechnicalArt\\DccScriptingInterface\\SDK", - "DCCSI_WING_VERSION_MAJOR": "7", - "DCCSI_WING_VERSION_MINOR": "1", - "WINGHOME": "C:\\Program Files (x86)\\Wing Pro 7.1", - "DCCSI_PY_DEFAULT": "G:\\depot\\JG_PC1_spectrAtom\\dev\\Tools\\Python\\3.7.5\\windows\\python.exe" -} diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/__init__.py index ab425ef427..1d552e7ea7 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/__init__.py @@ -12,23 +12,27 @@ # importing all of the modules """azpy.shared.common.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': _PACKAGENAME = 'azpy.shared.common' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- # diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/core_utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/core_utils.py index 4040620002..629f52bbd7 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/core_utils.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/core_utils.py @@ -48,34 +48,34 @@ import os import sys import site import fnmatch +import logging as _logging # 3rd Party from pathlib import Path # from progress.spinner import Spinner # deprecate use (or refactor) - -# Lumberyard extensions -from azpy.constants import * -from azpy import initialize_logger # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -# global space debug flag -from azpy.env_bool import env_bool +# global space +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) -_PACKAGENAME = __name__ -if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.shared.common.core_utils' +_MODULENAME = __name__ +if _MODULENAME is '__main__': + _MODULENAME = 'azpy.shared.common.core_utils' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_MODULENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) # ------------------------------------------------------------------------- @@ -421,7 +421,7 @@ def return_stub(stub): if __name__ == "__main__": '''To Do: Document''' # constants for shared use. - _G_DEBUG = True + _DCCSI_GDEBUG = True # happy _LOGGER.info _LOGGER.info("# {0} #".format('-' * 72)) @@ -435,7 +435,7 @@ if __name__ == "__main__": _KNOWN_SITEDIR_PATHS = site._init_pathinfo() # this is just a debug developer convenience _LOGGER.info (for testing acess) - if _G_DEBUG: + if _DCCSI_GDEBUG: import pkgutil _LOGGER.info('Current working dir: {0}'.format(cwd)) search_path = ['.'] # set to None to see all modules importable from sys.path diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/envar_utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/envar_utils.py index fc99df0f30..fc2db6be0c 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/envar_utils.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/envar_utils.py @@ -12,7 +12,7 @@ from __future__ import unicode_literals # ------------------------------------------------------------------------- ''' -Module: \azpy\shared\common\config_utils.py +Module: \\azpy\\shared\\common\\config_utils.py A set of utility functions @@ -33,28 +33,31 @@ import logging as _logging # 3rd Party from box import Box from unipath import Path - -# Lumberyard extensions -from azpy.constants import * # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -from azpy.env_bool import env_bool +# global space +import azpy.env_bool as env_bool +from azpy.constants import ENVAR_O3DE_DEV +from azpy.constants import ENVAR_O3DE_PROJECT from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) -_PACKAGENAME = __name__ -if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.shared.common.envar_utils' +_MODULENAME = __name__ +if _MODULENAME is '__main__': + _MODULENAME = 'azpy.shared.common.envar_utils' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_MODULENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) # ------------------------------------------------------------------------- @@ -65,7 +68,7 @@ def get_envar_default(envar, envar_default=None, envar_set=Box(ordered_box=True) Get from the system environment, or the module dictionary (a Box): like the test one in __main__ below, TEST_ENV_VALUES = Box(ordered_box=True) - TEST_ENV_VALUES[ENVAR_LY_PROJECT] = '${0}'.format(ENVAR_LY_PROJECT) + TEST_ENV_VALUES[ENVAR_O3DE_PROJECT] = '${0}'.format(ENVAR_O3DE_PROJECT) This dictionary provides a simple way to pack a default set into a structure and decouple the getter implementation. @@ -88,7 +91,7 @@ def get_envar_default(envar, envar_default=None, envar_set=Box(ordered_box=True) # -- envar util ---------------------------------------------------------- -def set_envar_defaults(envar_set, env_root=get_envar_default(ENVAR_LY_DEV)): +def set_envar_defaults(envar_set, env_root=get_envar_default(ENVAR_O3DE_DEV)): """ Set each environment variable if not alreay set with value. Must be safe, will not over-write existing. @@ -98,8 +101,8 @@ def set_envar_defaults(envar_set, env_root=get_envar_default(ENVAR_LY_DEV)): env_root = Path(env_root) if env_root.exists(): - os.environ[ENVAR_LY_DEV] = env_root - envar_set[ENVAR_LY_DEV] = env_root + os.environ[ENVAR_O3DE_DEV] = env_root + envar_set[ENVAR_O3DE_DEV] = env_root else: raise ValueError("EnvVar Root is not valid: {0}".format(env_root)) @@ -107,7 +110,7 @@ def set_envar_defaults(envar_set, env_root=get_envar_default(ENVAR_LY_DEV)): envar = str(envar) value = os.getenv(envar) - if _G_DEBUG: + if _DCCSI_GDEBUG: if not value: _LOGGER.debug('~ EnVar value NOT found: {0}\r'.format(envar)) @@ -191,8 +194,8 @@ if __name__ == '__main__': # it should be benign but leaving this comment here in case of funk # tes envars - TEST_ENV_VALUES[ENVAR_LY_PROJECT] = '${0}'.format(ENVAR_LY_PROJECT) - TEST_ENV_VALUES[ENVAR_LY_DEV] = Path('${0}'.format(ENVAR_LY_DEV)) + TEST_ENV_VALUES[ENVAR_O3DE_PROJECT] = '${0}'.format(ENVAR_O3DE_PROJECT) + TEST_ENV_VALUES[ENVAR_O3DE_DEV] = Path('${0}'.format(ENVAR_O3DE_DEV)) # try to fetch and set the base values from the environment # this makes sure all envars set, are resolved on import @@ -204,7 +207,7 @@ if __name__ == '__main__': ensure_ascii=False), '\r') # simple tests - _ENV_TAG = 'LY_DEV' + _ENV_TAG = 'O3DE_DEV' foo = get_envar_default(_ENV_TAG) _LOGGER.info("~ Results of getVar on tag, '{0}':'{1}'\r".format(_ENV_TAG, foo)) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/__init__.py index c18cf9d81a..36a354a965 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/__init__.py @@ -13,31 +13,35 @@ SPDX-License-Identifier: Apache-2.0 OR MIT import os from pathlib import Path import logging as _logging +# ------------------------------------------------------------------------- -from azpy.env_bool import env_bool +# ------------------------------------------------------------------------- +# global space +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': _PACKAGENAME = 'azpy.shared.noodely' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) -# ------------------------------------------------------------------------- -# +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) + __all__ = ['find_arg', 'helpers', 'node', 'synth', 'synth_arg_kwarg', 'test_foo'] -# # ------------------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/find_arg.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/find_arg.py index 681c88b33c..dd939f3973 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/find_arg.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/find_arg.py @@ -74,7 +74,7 @@ if __name__ == "__main__": print ('~ find_arg.py ... Running script as __main__') print ("# ----------------------------------------------------------------------- #\r") - _G_DEBUG = True + _DCCSI_GDEBUG = True from test_foo import Foo @@ -102,7 +102,7 @@ if __name__ == "__main__": self._kwargsDict[key] = value # synthesize(self, '{0}'.format(key), value) <-- I have a method, # which synthesizes properties... with gettr, settr, etc. - if _G_DEBUG: + if _DCCSI_GDEBUG: print("{0}:{1}".format(key, value)) # representation diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/node.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/node.py index ca44b9a6c8..59e25d45e1 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/node.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/node.py @@ -47,13 +47,13 @@ from azpy.constants import ENVAR_DCCSI_DEV_MODE # global space # To Do: update to dynaconf dynamic env and settings? -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _MODULENAME = 'azpy.shared.noodely.node' _log_level = int(20) -if _G_DEBUG: +if _DCCSI_GDEBUG: _log_level = int(10) _LOGGER = azpy.initialize_logger(_MODULENAME, log_to_file=False, @@ -66,7 +66,7 @@ _LOGGER.debug('Starting:: {}.'.format({_MODULENAME})) # quick test code (remove later) from hashids import Hashids hashids = Hashids(min_length=16, salt='DCCsi') -if _G_DEBUG: +if _DCCSI_GDEBUG: print (hashids.encrypt(193487)) # test hash # ------------------------------------------------------------------------- @@ -107,7 +107,7 @@ class Node(object): """Class constructor: makes a node.""" # share the debug state - _DEBUG = _G_DEBUG + _DEBUG = _DCCSI_GDEBUG # logger _LOGGER = _G_LOGGER diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/pathnode.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/pathnode.py index 5c5402cb7d..401e524060 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/pathnode.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/pathnode.py @@ -47,13 +47,13 @@ from azpy.constants import ENVAR_DCCSI_DEV_MODE # ------------------------------------------------------------------------- # global space # To Do: update to dynaconf dynamic env and settings? -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _MODULENAME = 'azpy.shared.noodely.pathnode' _log_level = int(20) -if _G_DEBUG: +if _DCCSI_GDEBUG: _log_level = int(10) _LOGGER = azpy.initialize_logger(_MODULENAME, log_to_file=False, @@ -78,7 +78,7 @@ class PathNode(Node): """doc string""" # share the debug state - _DEBUG = _G_DEBUG + _DEBUG = _DCCSI_GDEBUG # class header _message_header = 'noodly, PathNode(): Message' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/__init__.py index a73ee00d7c..745ec9a7be 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/__init__.py @@ -11,24 +11,26 @@ """azpy.shared.ui.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': _PACKAGENAME = 'azpy.shared.ui' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) - +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- # __all__ = [] diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/base_widget.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/base_widget.py index 311ed484ed..493130eb64 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/base_widget.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/base_widget.py @@ -34,7 +34,7 @@ from shiboken2 import wrapInstance # ------------------------------------------------------------------------- # global space debug flag -_G_DEBUG = settings.DCCSI_GDEBUG +_DCCSI_GDEBUG = settings.DCCSI_GDEBUG # global space debug flag _DCCSI_DEV_MODE = settings.DCCSI_DEV_MODE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/custom_treemodel.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/custom_treemodel.py index d7b096468d..871b050c7a 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/custom_treemodel.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/custom_treemodel.py @@ -30,7 +30,7 @@ import PySide2.QtGui as QtGui # ------------------------------------------------------------------------- # global space # To Do: update to dynaconf dynamic env and settings? -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _MODULE_PATH = Path(__file__) @@ -38,7 +38,7 @@ _MODULE_PATH = Path(__file__) _MODULENAME = 'azpy.shared.ui.custom_treemodel' _log_level = int(20) -if _G_DEBUG: +if _DCCSI_GDEBUG: _log_level = int(10) _LOGGER = azpy.initialize_logger(_MODULENAME, log_to_file=False, diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/help_menu.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/help_menu.py index f4b53f6b79..7fdeadc4c2 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/help_menu.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/help_menu.py @@ -27,7 +27,7 @@ import PySide2.QtWidgets as QtWidgets # ------------------------------------------------------------------------- # global space debug flag -_G_DEBUG = os.getenv('DCCSI_GDEBUG', False) +_DCCSI_GDEBUG = os.getenv('DCCSI_GDEBUG', False) # global space developer mode flag _DCCSI_DEV_MODE = os.getenv('DCCSI_DEV_MODE', False) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_qtextedit_stdout.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_qtextedit_stdout.py index 6fd4f359bc..d14d5bcbd6 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_qtextedit_stdout.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_qtextedit_stdout.py @@ -32,7 +32,7 @@ from PySide2.QtCore import QTimer # ------------------------------------------------------------------------- # global space debug flag -_G_DEBUG = os.getenv('DCCSI_GDEBUG', False) +_DCCSI_GDEBUG = os.getenv('DCCSI_GDEBUG', False) # global space debug flag _DCCSI_DEV_MODE = os.getenv('DCCSI_DEV_MODE', False) @@ -48,7 +48,7 @@ _MODULENAME = __name__ if _MODULENAME is '__main__': _MODULENAME = _TOOL_TAG -if _G_DEBUG: +if _DCCSI_GDEBUG: _LOGGER = initialize_logger(_MODULENAME, log_to_file=True) _LOGGER.debug('Something invoked :: {0}.'.format({_MODULENAME})) else: @@ -130,7 +130,7 @@ if __name__ == '__main__': _TOOL_TAG = 'azpy.shared.ui.pyside2_qtextedit_stdout' _TYPE_TAG = 'test' - if _G_DEBUG: + if _DCCSI_GDEBUG: _LOGGER = initialize_logger('{0}-TEST'.format(_TOOL_TAG), log_to_file=True) _LOGGER.debug('Something invoked :: {0}.'.format({_MODULENAME})) @@ -155,7 +155,7 @@ if __name__ == '__main__': _READER.start('python', ['-u', _TEST_PY_FILE]) # start the process # after that starts, this will show the console - # LY_QSS = Path(_MODULE_PATH.parent, 'resources', 'stylesheets', 'LYstyle.qss') + # O3DE_QSS = Path(_MODULE_PATH.parent, 'resources', 'stylesheets', 'LYstyle.qss') _DARK_STYLE = Path(_MODULE_PATH.parent, 'resources', 'qdarkstyle', 'style.qss') _CONSOLE.qapp.setStyleSheet(_DARK_STYLE.read_file()) _CONSOLE.show() # make the console visible diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_ui_utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_ui_utils.py index b996d9baf6..0f229e45b6 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_ui_utils.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_ui_utils.py @@ -51,7 +51,7 @@ import azpy.shared.ui.help_menu as help_menu # ------------------------------------------------------------------------- # global space debug flag -_G_DEBUG = settings.DCCSI_GDEBUG +_DCCSI_GDEBUG = settings.DCCSI_GDEBUG # global space debug flag _DCCSI_DEV_MODE = settings.DCCSI_DEV_MODE @@ -112,7 +112,7 @@ def from_ui_generate_form_and_base_class(filename, return_output=False): ui_file.exists() except FileNotFoundError as error: output += 'File does not exist: {0}/r'.format(error) - if _G_DEBUG: + if _DCCSI_GDEBUG: print(error) if return_output: return False, output diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/qt_settings.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/qt_settings.py index de2bc1aaf4..28f6e23ac2 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/qt_settings.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/qt_settings.py @@ -24,7 +24,7 @@ import azpy.config_utils _config = azpy.config_utils.get_dccsi_config() # ^ this is effectively an import and retreive of \config.py # init lumberyard Qy/PySide2 access -_config.init_ly_pyside(settings.LY_DEV) +_config.init_o3de_pyside(settings.O3DE_DEV) # now we can import lumberyards PySide2 import PySide2.QtCore as QtCore @@ -32,7 +32,7 @@ import PySide2.QtWidgets as QtWidgets # ------------------------------------------------------------------------- # global space debug flag -_G_DEBUG = settings.DCCSI_GDEBUG +_DCCSI_GDEBUG = settings.DCCSI_GDEBUG # global space debug flag _DCCSI_DEV_MODE = settings.DCCSI_DEV_MODE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/templates.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/templates.py index a3700e8cf4..ec1d00b6d0 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/templates.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/templates.py @@ -47,7 +47,7 @@ import PySide2.QtUiTools as QtUiTools # ------------------------------------------------------------------------- # global space debug flag -_G_DEBUG = settings.DCCSI_GDEBUG +_DCCSI_GDEBUG = settings.DCCSI_GDEBUG # global space debug flag _DCCSI_DEV_MODE = settings.DCCSI_DEV_MODE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/synthetic_env.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/synthetic_env.py index 71f2682242..33ac6c6814 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/synthetic_env.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/synthetic_env.py @@ -40,12 +40,11 @@ Configures several useful environment config settings and paths, [key] : [value] # this is the required base environment - LY_PROJECT : name of project (project directory) - LY_DEV : path to Lumberyard \dev root - LY_PROJECT_PATH : path to project dir + O3DE_PROJECT : name of project (project directory) + O3DE_DEV : path to Lumberyard \dev root + O3DE_PROJECT_PATH : path to project dir DCCSIG_PATH : path to the DCCsi Gem root - DCCSI_AZPY_PATH * : path to azpy Python API (code) - DCCSI_SDK_PATH : path to associated (non-api code) DCC SDK + DCCSI_TOOLS_PATH : path to associated (non-api code) DCC SDK # nice to haves in base env to define core support DCCSI_GDEBUG : sets global debug prints @@ -59,7 +58,7 @@ Configures several useful environment config settings and paths, :: Default version py37 has a launcher (activates the env, starts py interpreter) - set DCCSI_PY_BASE=%DCCSI_PYTHON_INSTALL%\python.exe + set DCCSI_PY_BASE=%O3DE_PYTHON_INSTALL%\python.exe :: shared location for 64bit python 3.7 BASE location set DCCSI_PY_DCCSI=%DCCSI_LAUNCHERS_PATH%\Launch_pyBASE.bat @@ -74,7 +73,7 @@ Configures several useful environment config settings and paths, :: shared location for 64bit DCCSI_PY_MAYA python 2.7 DEV location set DCCSI_PY_MAYA=%MAYA_LOCATION%\bin\mayapy.exe :: wingIDE can use more then one defined/managed interpreters - :: allowing you to _G_DEBUG code in multiple runtimes in one session + :: allowing you to _DCCSI_GDEBUG code in multiple runtimes in one session ${DCCSI_PY_MAYA} # related to the WING as the default DCCSI_GDEBUGGER @@ -92,8 +91,9 @@ import os import sys import site import re -#import inspect +import inspect import json +import importlib.util import logging as _logging from collections import OrderedDict @@ -110,35 +110,33 @@ _MODULE_PATH = os.path.realpath(__file__) # To Do: what if frozen? _DCCSIG_PATH = os.path.normpath(os.path.join(_MODULE_PATH, '../..')) _DCCSIG_PATH = os.getenv('DCCSIG_PATH', _DCCSIG_PATH) site.addsitedir(_DCCSIG_PATH) -print(_DCCSIG_PATH) # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -# Lumberyard extensions +# O3DE extensions from pathlib import Path # set up global space, logging etc. -import azpy -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) -_PACKAGENAME = 'DCCsi.azpy.sunthetic_env' +_PACKAGENAME = 'DCCsi.azpy.synthetic_env' -_log_level = int(20) -if _G_DEBUG: - _log_level = int(10) -_LOGGER = azpy.initialize_logger(_PACKAGENAME, - log_to_file=True, - default_log_level=_log_level) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) -_LOGGER.debug('Starting up: {0}.'.format({_PACKAGENAME})) _LOGGER.debug('_DCCSIG_PATH: {}'.format(_DCCSIG_PATH)) -_LOGGER.debug('_G_DEBUG: {}'.format(_G_DEBUG)) +_LOGGER.debug('_DCCSI_GDEBUG: {}'.format(_DCCSI_GDEBUG)) _LOGGER.debug('_DCCSI_DEV_MODE: {}'.format(_DCCSI_DEV_MODE)) if _DCCSI_DEV_MODE: @@ -170,7 +168,7 @@ if os.path.exists(_DCCSI_PYTHON_LIB_PATH): # ------------------------------------------------------------------------- # post-bootstrap global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) # ------------------------------------------------------------------------- @@ -225,7 +223,7 @@ def return_stub(stub='dccsi_stub'): break if (len(tail) == 0): path = "" - if _G_DEBUG: + if _DCCSI_GDEBUG: _LOGGER.debug('~Not able to find the path to that file ' '(stub) in a walk-up from currnet path.') break @@ -262,7 +260,7 @@ def get_stub_check_path(in_path, check_stub='engineroot.txt'): # ------------------------------------------------------------------------- # TO DO: Move to a util package or module -def resolve_envar_path(envar='LY_DEV', +def resolve_envar_path(envar='O3DE_DEV', start_path=__file__, check_stub='engineroot.txt', dir_name='dev', @@ -276,7 +274,7 @@ def resolve_envar_path(envar='LY_DEV', That is a pretty safe indicator that we found the right '\dev' - Second it checks if the env var 'LY_DEV' is set, use that instead! + Second it checks if the env var 'O3DE_DEV' is set, use that instead! """ @@ -360,14 +358,14 @@ def stash_env(_SYNTH_ENV_DICT = OrderedDict()): # \dev Lumberyard ROOT PATH # someone decided to use this as a root stub (for similar reasons in C++?) - # STUB_LY_DEV = str('engineroot.txt') + # STUB_O3DE_DEV = str('engineroot.txt') # I don't own \dev so I didn't want to check in anything new there - _LY_DEV = resolve_envar_path(ENVAR_LY_DEV, # envar + _O3DE_DEV = resolve_envar_path(ENVAR_O3DE_DEV, # envar _THIS_MODULE_PATH, # search path - STUB_LY_DEV, # stub - TAG_DIR_LY_DEV) # dir + STUB_O3DE_DEV, # stub + TAG_DIR_O3DE_DEV) # dir - _SYNTH_ENV_DICT[ENVAR_LY_DEV] = _LY_DEV.as_posix() + _SYNTH_ENV_DICT[ENVAR_O3DE_DEV] = _O3DE_DEV.as_posix() # project name is a string, it should be project dir name # for siloed testing and a purely synthetc env (nothing previously set) @@ -376,17 +374,17 @@ def stash_env(_SYNTH_ENV_DICT = OrderedDict()): # for testing overrides of the default synthetic env # we can do two things here, - # first we can try to fetch from the env os.getenv('LY_PROJECT') + # first we can try to fetch from the env os.getenv('O3DE_PROJECT') # If comes back None, allows you to specify a default fallback # changed to just make the fallback what is set in boostrap # so now it's less of a fallnack and more correct if not # explicitly set - _LY_PROJECT = os.getenv(ENVAR_LY_PROJECT) - _SYNTH_ENV_DICT[ENVAR_LY_PROJECT] = _LY_PROJECT + _O3DE_PROJECT = os.getenv(ENVAR_O3DE_PROJECT) + _SYNTH_ENV_DICT[ENVAR_O3DE_PROJECT] = _O3DE_PROJECT - _LY_BUILD_DIR_NAME = os.getenv(ENVAR_LY_BUILD_DIR_NAME, - TAG_DIR_LY_BUILD) - _SYNTH_ENV_DICT[ENVAR_LY_BUILD_DIR_NAME] = _LY_BUILD_DIR_NAME + _O3DE_BUILD_DIR_NAME = os.getenv(ENVAR_O3DE_BUILD_DIR_NAME, + TAG_DIR_O3DE_BUILD_FOLDER) + _SYNTH_ENV_DICT[ENVAR_O3DE_BUILD_DIR_NAME] = _O3DE_BUILD_DIR_NAME # pattern for the above is (and will be repeated) # _SOME_ENVAR = resolve_envar_path('ENVAR', @@ -406,31 +404,31 @@ def stash_env(_SYNTH_ENV_DICT = OrderedDict()): # so we guess based on how I set up the original dev environment # -- envar -- - _LY_BUILD_PATH = Path(os.getenv(ENVAR_LY_BUILD_PATH, - PATH_LY_BUILD_PATH)) - _SYNTH_ENV_DICT[ENVAR_LY_BUILD_PATH] = _LY_BUILD_PATH.as_posix() + _O3DE_BUILD_PATH = Path(os.getenv(ENVAR_O3DE_BUILD_PATH, + PATH_O3DE_BUILD_PATH)) + _SYNTH_ENV_DICT[ENVAR_O3DE_BUILD_PATH] = _O3DE_BUILD_PATH.as_posix() # -- envar -- - _LY_BIN_PATH = Path(os.getenv(ENVAR_LY_BIN_PATH, - PATH_LY_BIN_PATH)) + _O3DE_BIN_PATH = Path(os.getenv(ENVAR_O3DE_BIN_PATH, + PATH_O3DE_BIN_PATH)) # some of these need hard checks - if not _LY_BIN_PATH.exists(): - raise Exception('LY_BIN_PATH does NOT exist: {0}'.format(_LY_BIN_PATH)) + if not _O3DE_BIN_PATH.exists(): + raise Exception('O3DE_BIN_PATH does NOT exist: {0}'.format(_O3DE_BIN_PATH)) else: - _SYNTH_ENV_DICT[ENVAR_LY_BIN_PATH] = _LY_BIN_PATH.as_posix() + _SYNTH_ENV_DICT[ENVAR_O3DE_BIN_PATH] = _O3DE_BIN_PATH.as_posix() # adding to sys.path apparently doesn't work for .dll locations like Qt - os.environ['PATH'] = _LY_BIN_PATH.as_posix() + os.pathsep + os.environ['PATH'] + os.environ['PATH'] = _O3DE_BIN_PATH.as_posix() + os.pathsep + os.environ['PATH'] # -- envar -- # if that stub marker doesn't exist assume DCCsi path (fallback 1) - _LY_PROJECT_PATH = Path(os.getenv(ENVAR_LY_PROJECT_PATH, - Path(_LY_DEV, _LY_PROJECT))) - _SYNTH_ENV_DICT[ENVAR_LY_PROJECT_PATH] = _LY_PROJECT_PATH.as_posix() + _O3DE_PROJECT_PATH = Path(os.getenv(ENVAR_O3DE_PROJECT_PATH, + Path(_O3DE_DEV, _O3DE_PROJECT))) + _SYNTH_ENV_DICT[ENVAR_O3DE_PROJECT_PATH] = _O3DE_PROJECT_PATH.as_posix() # -- envar -- _DCCSIG_PATH = resolve_envar_path(ENVAR_DCCSIG_PATH, # envar _THIS_MODULE_PATH, # search path - STUB_LY_ROOT_DCCSI, # stub name + STUB_O3DE_ROOT_DCCSI, # stub name TAG_DEFAULT_PROJECT) # dir _SYNTH_ENV_DICT[ENVAR_DCCSIG_PATH] = _DCCSIG_PATH.as_posix() @@ -440,9 +438,9 @@ def stash_env(_SYNTH_ENV_DICT = OrderedDict()): _SYNTH_ENV_DICT[ENVAR_DCCSI_AZPY_PATH] = _AZPY_PATH.as_posix() # -- envar -- - _DCCSI_SDK_PATH = Path(os.getenv(ENVAR_DCCSI_SDK_PATH, - Path(_DCCSIG_PATH, TAG_DIR_DCCSI_SDK))) - _SYNTH_ENV_DICT[ENVAR_DCCSI_SDK_PATH] = _DCCSI_SDK_PATH.as_posix() + _DCCSI_TOOLS_PATH = Path(os.getenv(ENVAR_DCCSI_TOOLS_PATH, + Path(_DCCSIG_PATH, TAG_DIR_DCCSI_TOOLS))) + _SYNTH_ENV_DICT[ENVAR_DCCSI_TOOLS_PATH] = _DCCSI_TOOLS_PATH.as_posix() # -- envar -- # external dccsi site-packages @@ -497,7 +495,7 @@ def init_ly_pyside(env_dict=_SYNTH_ENV_DICT): - QTFORPYTHON_PATH = Path.joinpath(LY_DEV, + QTFORPYTHON_PATH = Path.joinpath(O3DE_DEV, 'Gems', 'QtForPython', '3rdParty', @@ -509,16 +507,16 @@ def init_ly_pyside(env_dict=_SYNTH_ENV_DICT): sys.path.insert(1, str(QTFORPYTHON_PATH)) site.addsitedir(str(QTFORPYTHON_PATH)) - LY_BIN_PATH = Path.joinpath(LY_DEV, + O3DE_BIN_PATH = Path.joinpath(O3DE_DEV, 'windows_vs2019', 'bin', 'profile').resolve() - os.environ["DYNACONF_LY_BIN_PATH"] = str(LY_BIN_PATH) - os.environ["LY_BIN_PATH"] = str(LY_BIN_PATH) - site.addsitedir(str(LY_BIN_PATH)) - sys.path.insert(1, str(LY_BIN_PATH)) + os.environ["DYNACONF_O3DE_BIN_PATH"] = str(O3DE_BIN_PATH) + os.environ["O3DE_BIN_PATH"] = str(O3DE_BIN_PATH) + site.addsitedir(str(O3DE_BIN_PATH)) + sys.path.insert(1, str(O3DE_BIN_PATH)) - QT_PLUGIN_PATH = Path.joinpath(LY_BIN_PATH, + QT_PLUGIN_PATH = Path.joinpath(O3DE_BIN_PATH, 'EditorPlugins').resolve() os.environ["DYNACONF_QT_PLUGIN_PATH"] = str(QT_PLUGIN_PATH) os.environ["QT_PLUGIN_PATH"] = str(QT_PLUGIN_PATH) @@ -536,7 +534,7 @@ def init_ly_pyside(env_dict=_SYNTH_ENV_DICT): if sys.platform.startswith('win'): path = os.environ['PATH'] newPath = '' - newPath += str(LY_BIN_PATH) + os.pathsep + newPath += str(O3DE_BIN_PATH) + os.pathsep newPath += str(Path.joinpath(QTFORPYTHON_PATH, 'shiboken2').resolve()) + os.pathsep newPath += str(Path.joinpath(QTFORPYTHON_PATH, @@ -591,12 +589,12 @@ def set_env(dict_object): def test_Qt(): try: import PySide2 - print('PySide2: {0}'.format(Path(PySide2.__file__).as_posix())) + _LOGGER.info('PySide2: {0}'.format(Path(PySide2.__file__).as_posix())) # builtins.ImportError: DLL load failed: The specified procedure could not be found. from PySide2 import QtCore from PySide2 import QtWidgets except IOError as e: - print('ERROR: {0}'.format(e)) + _LOGGER.error('ERROR: {0}'.format(e)) raise e try: @@ -610,7 +608,7 @@ def test_Qt(): qapp.instance().quit qapp.exit() except Exception as e: - print('ERROR: {0}'.format(e)) + _LOGGER.error('ERROR: {0}'.format(e)) raise e # ------------------------------------------------------------------------- @@ -620,14 +618,14 @@ def main(argv, env_dict_object, debug=False, devmode=False): import getopt try: opts, args = getopt.getopt(argv, "hvt:", ["verbose=", "test="]) - except getopt.GetoptError: + except getopt.GetoptError as e: # not logging, print to cmd line console - print('synthetic_env.py -v -t ') + _LOGGER.error('synthetic_env.py -v -t ') sys.exit(2) for opt, arg in opts: if opt == '-h': - print('synthetic_env.py -v -t ') + _LOGGER.info('synthetic_env.py -v -t ') sys.exit() elif opt in ("-t", "--test"): @@ -639,14 +637,14 @@ def main(argv, env_dict_object, debug=False, devmode=False): try: from box import Box except ImportError as e: - print('ERROR: {0}'.format(e)) + _LOGGER.error('ERROR: {0}'.format(e)) raise e try: env_dict_object = Box(env_dict_object) - print(str(env_dict_object.to_json(sort_keys=False, + _LOGGER.info(str(env_dict_object.to_json(sort_keys=False, indent=4))) except Exception as e: - print('ERROR: {0}'.format(e)) + _LOGGER.error('ERROR: {0}'.format(e)) raise e # ------------------------------------------------------------------------- @@ -656,16 +654,16 @@ def main(argv, env_dict_object, debug=False, devmode=False): # ------------------------------------------------------------------------- if __name__ == '__main__': # run simple tests? - _G_DEBUG = True + _DCCSI_GDEBUG = True _DCCSI_DEV_MODE = True if _DCCSI_DEV_MODE: try: import azpy.test.entry_test - print('SUCCESS: import azpy.test.entry_test') + _LOGGER.info('SUCCESS: import azpy.test.entry_test') azpy.test.entry_test.main(verbose=True, connect_debugger=True) except ImportError as e: - print('ERROR: {0}'.format(e)) + _LOGGER.error('ERROR: {0}'.format(e)) raise e # init, stash and then activate @@ -673,9 +671,9 @@ if __name__ == '__main__': _SYNTH_ENV_DICT = stash_env(_SYNTH_ENV_DICT) _SYNTH_ENV_DICT = set_env(_SYNTH_ENV_DICT) - main(sys.argv[1:], _SYNTH_ENV_DICT, _G_DEBUG, _DCCSI_DEV_MODE) + main(sys.argv[1:], _SYNTH_ENV_DICT, _DCCSI_GDEBUG, _DCCSI_DEV_MODE) - if _G_DEBUG: + if _DCCSI_GDEBUG: tempBoxJsonFilePath = Path(_SYNTH_ENV_DICT['DCCSIG_PATH'], '.temp') tempBoxJsonFilePath = Path(tempBoxJsonFilePath, 'boxDumpTest.json') diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/__init__.py index 20df0e5813..554d320654 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/__init__.py @@ -12,39 +12,28 @@ # importing all of the modules """azpy.test.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': _PACKAGENAME = 'azpy.test' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) - -# ------------------------------------------------------------------------- +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) __all__ = ['entry_test'] - -# ------------------------------------------------------------------------- - - -# ------------------------------------------------------------------------- -if _DCCSI_DEV_MODE: - # If in dev mode this will test imports of __all__ - from azpy import test_imports - _LOGGER.debug('Testing Imports from {0}'.format(_PACKAGENAME)) - test_imports(__all__, - _pkg=_PACKAGENAME, - _logger=_LOGGER) # ------------------------------------------------------------------------- @@ -53,15 +42,26 @@ def init(): """If the substance api is required for a package/module to import, then it should be initialized and added here so general imports don't fail""" - - # __all__.append() # Make sure we can import the native apis # import + # __all__.append() + # Importing local packages/modules pass +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +if _DCCSI_DEV_MODE: + # If in dev mode this will test imports of __all__ + from azpy import test_imports + _LOGGER.debug('Testing Imports from {0}'.format(_PACKAGENAME)) + test_imports(__all__, + _pkg=_PACKAGENAME, + _logger=_LOGGER) # ------------------------------------------------------------------------- + del _LOGGER diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/entry_test.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/entry_test.py index d9d136f96e..aad440d11f 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/entry_test.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/entry_test.py @@ -11,9 +11,9 @@ from __future__ import unicode_literals # ------------------------------------------------------------------------- -import sys import os import site +import logging as _logging # note: some modules not available in py2.7 unless we boostrap with config.py # See example: @@ -23,34 +23,34 @@ from pathlib import Path # ------------------------------------------------------------------------- _BOOT_CHECK = False # set true to test breakpoint in this module directly -import azpy -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -# To Do: update to dynaconf dynamic env and settings? -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) -_MODULENAME = 'azpy.test.entry_test' +_MODULENAME = __name__ +if _MODULENAME is '__main__': + _MODULENAME = 'azpy.test.entry_test' -_log_level = int(20) -if _G_DEBUG: - _log_level = int(10) -_LOGGER = azpy.initialize_logger(_MODULENAME, - log_to_file=False, - default_log_level=_log_level) -_LOGGER.debug('Starting:: {}.'.format({_MODULENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_MODULENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -def main(verbose=_G_DEBUG, connect_debugger=True): - _LOGGER.info('{}'.format('-' * 74)) - _LOGGER.info('entry_test.main()') - _LOGGER.info('Root test import successful:') - _LOGGER.info('~ {}'.format(__file__)) +def main(verbose=_DCCSI_GDEBUG, connect_debugger=True): + if verbose: + _LOGGER.info('{}'.format('-' * 74)) + _LOGGER.info('entry_test.main()') + _LOGGER.info('Root test import successful:') + _LOGGER.info('~ {}'.format(__file__)) if connect_debugger: status = connect_wing() @@ -67,7 +67,7 @@ def connect_wing(): _WINGHOME = os.environ['WINGHOME'] # test _LOGGER.info('~ WINGHOME: {0}'.format(_WINGHOME)) except Exception as e: - _LOGGER.warning(e) + _LOGGER.info(e) from azpy.constants import PATH_DEFAULT_WINGHOME _WINGHOME = PATH_DEFAULT_WINGHOME os.environ['WINGHOME'] = PATH_DEFAULT_WINGHOME @@ -134,5 +134,5 @@ def connect_wing(): # Main Code Block, runs this script as main (testing) # ------------------------------------------------------------------------- if __name__ == '__main__': - _G_DEBUG = True - main(verbose=_G_DEBUG, connect_debugger=_G_DEBUG) + _DCCSI_GDEBUG = True + main(verbose=_DCCSI_GDEBUG, connect_debugger=_DCCSI_GDEBUG) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/config.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/config.py index 0c25d16133..5578bcd577 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/config.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/config.py @@ -6,8 +6,11 @@ SPDX-License-Identifier: Apache-2.0 OR MIT """ # ------------------------------------------------------------------------- -"""Extend the .env using dynaconf (dynamic configuration and settings) -This config.py module assumes a minimal enviornment is defined in the .env +""".config +Generate dynamic and synethetic environment contest and settings +using dynaconf (dynamic configuration and settings) +This config.py synthetic env can be overriden or extended with a local .env +See: example.env.tmp (copy and rename to .env) To do: ensure that we can stack/layer the dynamic env to work with O3DE projects """ @@ -16,60 +19,97 @@ import os import sys import site import re +import logging as _logging # 3rdParty (possibly) py3 ships with pathlib, 2.7 does not # import pathlib # our framework for dcc tools need to run in apps like Maya that may still be # on py27 so we need to import and use after some boostrapping +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +_O3DE_RUNNING=None +try: + import azlmbr + _O3DE_RUNNING=True +except: + _O3DE_RUNNING=False +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def attach_debugger(): + _DCCSI_GDEBUG = True + os.environ["DYNACONF_DCCSI_GDEBUG"] = str(_DCCSI_GDEBUG) + + _DCCSI_DEV_MODE = True + os.environ["DYNACONF_DCCSI_DEV_MODE"] = str(_DCCSI_DEV_MODE) + + from azpy.test.entry_test import connect_wing + _debugger = connect_wing() + + return _debugger +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# global scope +_MODULENAME = __name__ +if _MODULENAME is '__main__': + _MODULENAME = 'DCCsi.config' -# -------------------------------------------------------------------+------ #os.environ['PYTHONINSPECT'] = 'True' _MODULE_PATH = os.path.abspath(__file__) # we don't have access yet to the DCCsi Lib\site-packages # (1) this will give us import access to azpy (always?) -_DCCSIG_PATH = os.getenv('DCCSIG_PATH', +_DCCSI_PATH = os.getenv('DCCSI_PATH', os.path.abspath(os.path.dirname(_MODULE_PATH))) # ^ we assume this config is in the root of the DCCsi -# if it's not, be sure to set envar 'DCCSIG_PATH' to ensure it -site.addsitedir(_DCCSIG_PATH) # PYTHONPATH +# if it's not, be sure to set envar 'DCCSI_PATH' to ensure it +site.addsitedir(_DCCSI_PATH) # must be done for azpy # now we have azpy api access import azpy from azpy.env_bool import env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import ENVAR_DCCSI_LOGLEVEL # set up global space, logging etc. # set these true if you want them set globally for debugging _DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) - -_PACKAGENAME = 'DCCsi.config' - -_LOG_LEVEL = int(20) +_DCCSI_LOGLEVEL = int(env_bool(ENVAR_DCCSI_LOGLEVEL, int(20))) if _DCCSI_GDEBUG: - _LOG_LEVEL = int(10) -_LOGGER = azpy.initialize_logger(_PACKAGENAME, - log_to_file=False, - default_log_level=_LOG_LEVEL) -_LOGGER.info('Starting up: {}.'.format({_PACKAGENAME})) -_LOGGER.info('site.addsitedir({})'.format(_DCCSIG_PATH)) -_LOGGER.debug('_DCCSI_GDEBUG: {}'.format(_DCCSI_GDEBUG)) -_LOGGER.debug('_DCCSI_DEV_MODE: {}'.format(_DCCSI_DEV_MODE)) + _DCCSI_LOGLEVEL = int(10) # early attach WingIDE debugger (can refactor to include other IDEs later) +# requires externally enabling via ENVAR if _DCCSI_DEV_MODE: - from azpy.test.entry_test import connect_wing - foo = connect_wing() + _debugger = attach_debugger() # to do: ^ this should be replaced with full featured azpy.dev.util # that supports additional debuggers (pycharm, vscode, etc.) + +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) + +_LOGGER = azpy.initialize_logger(_MODULENAME, + log_to_file=_DCCSI_GDEBUG, + default_log_level=_DCCSI_LOGLEVEL) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) +_LOGGER.info('site.addsitedir({})'.format(_DCCSI_PATH)) +_LOGGER.debug('_DCCSI_GDEBUG: {}'.format(_DCCSI_GDEBUG)) +_LOGGER.debug('_DCCSI_DEV_MODE: {}'.format(_DCCSI_DEV_MODE)) +_LOGGER.debug('_DCCSI_LOGLEVEL: {}'.format(_DCCSI_LOGLEVEL)) # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -# (2) this will give us import access to modules we provide -_DCCSI_PYTHON_LIB_PATH = azpy.config_utils.bootstrap_dccsi_py_libs(_DCCSIG_PATH) +# this will give us import access to additional modules we provide with DCCsi +_DCCSI_PYTHON_LIB_PATH = azpy.config_utils.bootstrap_dccsi_py_libs(_DCCSI_PATH) # Now we should be able to just carry on with pth lib and dynaconf from dynaconf import Dynaconf @@ -79,82 +119,103 @@ except: import pathlib2 as pathlib from pathlib import Path -_DCCSIG_PATH = Path(_DCCSIG_PATH).resolve() -_DCCSI_PYTHON_LIB_PATH = Path(_DCCSI_PYTHON_LIB_PATH).resolve() +_DCCSI_PATH = Path(_DCCSI_PATH) # pathify +_DCCSI_PYTHON_PATH = Path(_DCCSI_PATH,'3rdParty','Python') +_DCCSI_PYTHON_LIB_PATH = Path(_DCCSI_PYTHON_LIB_PATH) # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -def init_ly_pyside(LY_DEV=None): - """sets access to lumberyards Qt dlls and PySide""" +# start locally prepping known default values for dyanmic environment settings +_O3DE_DCCSI_PATH = os.environ['PATH'] +os.environ["DYNACONF_PATH"] = _O3DE_DCCSI_PATH + +# this will retreive the O3DE engine root +_O3DE_DEV = azpy.config_utils.get_o3de_engine_root() +# set up dynamic config envars +os.environ["DYNACONF_O3DE_DEV"] = str(_O3DE_DEV.resolve()) + +from azpy.constants import TAG_DIR_O3DE_BUILD_FOLDER +_O3DE_BUILD_FOLDER = TAG_DIR_O3DE_BUILD_FOLDER +os.environ["DYNACONF_O3DE_BUILD_FOLDER"] = str(_O3DE_BUILD_FOLDER) +_O3DE_BUILD_PATH = Path(_O3DE_DEV, TAG_DIR_O3DE_BUILD_FOLDER) +os.environ["DYNACONF_O3DE_BUILD_PATH"] = str(_O3DE_BUILD_PATH.resolve()) + +from azpy.constants import STR_O3DE_BIN_PATH +_O3DE_BIN_PATH = Path(STR_O3DE_BIN_PATH.format(_O3DE_BUILD_PATH)) +os.environ["DYNACONF_O3DE_BIN_PATH"] = str(_O3DE_BIN_PATH.resolve()) + +# this in most cases will return the project folder +# if it returns a matching engine folder then we don't know the project folder +_O3DE_PROJECT_PATH = azpy.config_utils.get_o3de_project_path() +os.environ["DYNACONF_O3DE_PROJECT_PATH"] = str(_O3DE_PROJECT_PATH.resolve()) + +# special, a home for stashing PYTHONPATHs into managed settings +_O3DE_PYTHONPATH = list() +_O3DE_PYTHONPATH.append(_DCCSI_PATH) +# ------------------------------------------------------------------------- + - LY_DEV = Path(LY_DEV).resolve() - if not LY_DEV.exists(): - raise Exception('LY_DEV does NOT exist: {0}'.format(LY_DEV)) +# ------------------------------------------------------------------------- +def init_o3de_pyside2(dccsi_path=_DCCSI_PATH, + engine_bin=_O3DE_BIN_PATH): + """Initialize the DCCsi Qt/PySide dynamic env and settings + sets access to lumberyards Qt dlls and PySide""" + + _DCCSI_PATH = Path(dccsi_path) + _O3DE_BIN_PATH = Path(engine_bin) + + if not _O3DE_BIN_PATH.exists(): + raise Exception('_O3DE_BIN_PATH does NOT exist: {0}'.format(_O3DE_BIN_PATH)) else: - # to do: 'windows_vs2019' might change or be different locally - # 'windows_vs2019' is defined as a str tag in constants - # we may not yet have access to azpy.constants :( - from azpy.constants import TAG_DIR_LY_BUILD - from azpy.constants import PATH_LY_BUILD_PATH - from azpy.constants import PATH_LY_BIN_PATH - # to do: pull some of these str and tags from constants - LY_BUILD_PATH = Path.joinpath(LY_DEV, - TAG_DIR_LY_BUILD).resolve() - LY_BIN_PATH = Path.joinpath(LY_BUILD_PATH, - 'bin', - 'profile').resolve() + pass + + # python config + _DCCSI_PYTHON_PATH = Path(_DCCSI_PATH,'3rdParty','Python') + os.environ["DYNACONF_DCCSI_PYTHON_PATH"] = str(_DCCSI_PYTHON_PATH.resolve()) # # allows to retreive from settings.QTFORPYTHON_PATH # from azpy.constants import STR_QTFORPYTHON_PATH # a path string constructor - # QTFORPYTHON_PATH = Path(STR_QTFORPYTHON_PATH.format(LY_DEV)).resolve() + # QTFORPYTHON_PATH = Path(STR_QTFORPYTHON_PATH.format(O3DE_DEV)).resolve() # os.environ["DYNACONF_QTFORPYTHON_PATH"] = str(QTFORPYTHON_PATH) # site.addsitedir(str(QTFORPYTHON_PATH)) # PYTHONPATH - QT_PLUGIN_PATH = Path.joinpath(LY_BIN_PATH, - 'EditorPlugins').resolve() - os.environ["DYNACONF_QT_PLUGIN_PATH"] = str(QT_PLUGIN_PATH) + QT_PLUGIN_PATH = Path.joinpath(_O3DE_BIN_PATH,'EditorPlugins') + os.environ["DYNACONF_QT_PLUGIN_PATH"] = str(QT_PLUGIN_PATH.resolve()) os.environ['PATH'] = QT_PLUGIN_PATH.as_posix() + os.pathsep + os.environ['PATH'] - QT_QPA_PLATFORM_PLUGIN_PATH = Path.joinpath(QT_PLUGIN_PATH, - 'platforms').resolve() - os.environ["DYNACONF_QT_QPA_PLATFORM_PLUGIN_PATH"] = str(QT_QPA_PLATFORM_PLUGIN_PATH) + QT_QPA_PLATFORM_PLUGIN_PATH = Path.joinpath(QT_PLUGIN_PATH, 'platforms') + os.environ["DYNACONF_QT_QPA_PLATFORM_PLUGIN_PATH"] = str(QT_QPA_PLATFORM_PLUGIN_PATH.resolve()) # if the line below is removed external standalone apps can't load PySide2 - os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = str(QT_QPA_PLATFORM_PLUGIN_PATH) + os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = str(QT_QPA_PLATFORM_PLUGIN_PATH.resolve()) # ^^ bypass trying to set only with DYNACONF environment os.environ['PATH'] = QT_QPA_PLATFORM_PLUGIN_PATH.as_posix() + os.pathsep + os.environ['PATH'] # ^^ this particular env only works correctly if put on the PATH in this manner # add Qt binaries to the Windows path to handle findings DLL file dependencies if sys.platform.startswith('win'): - # path = os.environ['PATH'] - # newPath = '' - # newPath += str(LY_BIN_PATH) + os.pathsep - # newPath += str(Path.joinpath(QTFORPYTHON_PATH, - # 'shiboken2').resolve()) + os.pathsep - # newPath += str(Path.joinpath(QTFORPYTHON_PATH, - # 'PySide2').resolve()) + os.pathsep - # newPath += path - # os.environ['PATH']=newPath - _LOGGER.debug('PySide2 bootstrapped PATH for Windows.') + _LOGGER.info('~ Qt/PySide2 bootstrapped PATH for Windows.') + else: + _LOGGER.warning('~ Not tested on Non-Windows platforms.') + # To Do: figure out how to test and/or modify to work try: import PySide2 - _LOGGER.debug('DCCsi, config.py: SUCCESS: import PySide2') + _LOGGER.info('~ SUCCESS: import PySide2') _LOGGER.debug(PySide2) status = True except ImportError as e: - _LOGGER.debug('DCCsi, config.py: FAILURE: import PySide2') + _LOGGER.error('~ FAILURE: import PySide2') status = False raise(e) try: import shiboken2 - _LOGGER.debug('DCCsi, config.py: SUCCESS: import shiboken2') + _LOGGER.info('~ SUCCESS: import shiboken2') _LOGGER.debug(shiboken2) status = True except ImportError as e: - _LOGGER.debug('DCCsi, config.py: FAILURE: import shiboken2') + _LOGGER.error('~ FAILURE: import shiboken2') status = False raise(e) @@ -162,19 +223,44 @@ def init_ly_pyside(LY_DEV=None): # to do: move path construction string to constants and build off of SDK # have not done that yet as I really want to get legal approval and # add this to the QtForPython Gem - # please pass this on the current code review - DCCSI_PYSIDE2_TOOLS = Path.joinpath(LY_DEV, - 'Gems', - 'AtomLyIntegration', - 'TechnicalArt', - 'DccScriptingInterface', - '.dev', - 'QtForPython', - 'pyside2-tools-dev') - os.environ["DYNACONF_DCCSI_PYSIDE2_TOOLS"] = str(DCCSI_PYSIDE2_TOOLS.resolve()) - os.environ['PATH'] = DCCSI_PYSIDE2_TOOLS.as_posix() + os.pathsep + os.environ['PATH'] - - return status + # please pass this in current code reviews + _DCCSI_PYSIDE2_TOOLS = Path(_DCCSI_PYTHON_PATH,'pyside2-tools') + if _DCCSI_PYSIDE2_TOOLS.exists(): + os.environ["DYNACONF_DCCSI_PYSIDE2_TOOLS"] = str(_DCCSI_PYSIDE2_TOOLS.resolve()) + os.environ['PATH'] = _DCCSI_PYSIDE2_TOOLS.as_posix() + os.pathsep + os.environ['PATH'] + + site.addsitedir(_DCCSI_PYSIDE2_TOOLS) + _O3DE_PYTHONPATH.append(_DCCSI_PYSIDE2_TOOLS.resolve()) + _LOGGER.info('~ PySide2-Tools bootstrapped PATH for Windows.') + try: + import pyside2uic + _LOGGER.info('~ SUCCESS: import pyside2uic') + _LOGGER.debug(shiboken2) + status = True + except ImportError as e: + _LOGGER.error('~ FAILURE: import pyside2uic') + status = False + raise(e) + else: + _LOGGER.warning('~ No PySide2 Tools: {}'.format(_DCCSI_PYSIDE2_TOOLS.resolve)) + + _O3DE_DCCSI_PATH = os.environ['PATH'] + os.environ["DYNACONF_PATH"] = _O3DE_DCCSI_PATH + + try: + _DCCSI_PYTHONPATH = os.environ['PYTHONPATH'] + os.environ["DYNACONF_PYTHONPATH"] = _DCCSI_PYTHONPATH + except: + pass + + from dynaconf import settings + + _LOGGER.info('~ config.init_o3de_pyside() ... DONE') + + if status: + return settings + else: + return None # ------------------------------------------------------------------------- @@ -182,90 +268,219 @@ def init_ly_pyside(LY_DEV=None): def test_pyside2(): """Convenience method to test Qt / PySide2 access""" # now test - _LOGGER.info('Testing Qt / PySide2') + _LOGGER.info('~ Testing Qt / PySide2') try: from PySide2.QtWidgets import QApplication, QPushButton app = QApplication(sys.argv) - hello = QPushButton("Hello world!") + hello = QPushButton("~ O3DE DCCsi PySide2 Test!") hello.resize(200, 60) hello.show() except Exception as e: - _LOGGER.error('FAILURE: Qt / PySide2') + _LOGGER.error('~ FAILURE: Qt / PySide2') status = False raise(e) - _LOGGER.info('SUCCESS: .test_pyside2()') + _LOGGER.info('~ SUCCESS: .test_pyside2()') sys.exit(app.exec_()) # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -# `envvar_prefix` = export envvars with `export DYNACONF_FOO=bar`. -# `settings_files` = Load this files in the order. -# here we are modifying or adding to the dynamic config settings on import -settings = Dynaconf( - envvar_prefix="DYNACONF", - settings_files=['settings.json', '.secrets.json'], -) - -from azpy.constants import PATH_LY_BUILD_PATH -from azpy.constants import PATH_LY_BIN_PATH - -# global settings -os.environ["DYNACONF_DCCSI_GDEBUG"] = str(_DCCSI_GDEBUG) -os.environ["DYNACONF_DCCSI_DEV_MODE"] = str(_DCCSI_DEV_MODE) - -# search up to get \dev -_LY_DEV = azpy.config_utils.get_stub_check_path(in_path=_DCCSIG_PATH, - check_stub='engine.json') -os.environ["DYNACONF_LY_DEV"] = str(_LY_DEV.resolve()) -_LY_PROJECT = azpy.config_utils.get_current_project() -os.environ["DYNACONF_LY_PROJECT"] = str(_LY_PROJECT.resolve()) -_LY_PROJECT_PATH = Path(_LY_DEV, _LY_PROJECT) -os.environ["DYNACONF_LY_PROJECT_PATH"] = str(_LY_PROJECT_PATH) -os.environ["DYNACONF_DCCSIG_PATH"] = str(_DCCSIG_PATH) -_DCCSI_CONFIG_PATH = Path(_MODULE_PATH).resolve() -os.environ["DYNACONF_DCCSI_CONFIG_PATH"] = str(_DCCSI_CONFIG_PATH) -_DCCSIG_SDK_PATH = Path.joinpath(_DCCSIG_PATH, 'SDK').resolve() -os.environ["DYNACONF_DCCSIG_SDK_PATH"] = str(_DCCSIG_SDK_PATH) -os.environ["DYNACONF_DCCSI_PYTHON_LIB_PATH"] = str(_DCCSI_PYTHON_LIB_PATH) -os.environ["DYNACONF_OS_FOLDER"] = azpy.config_utils.get_os() - -# we need to set up the Ly dev build \bin\path (for Qt dll access) -_LY_BUILD_PATH = Path(PATH_LY_BUILD_PATH).resolve() -os.environ["DYNACONF_LY_BUILD_PATH"] = str(_LY_BUILD_PATH) -_LY_BIN_PATH = Path(PATH_LY_BIN_PATH).resolve() -os.environ["DYNACONF_LY_BIN_PATH"] = str(_LY_BIN_PATH) - -# project cache log dir path -from azpy.constants import ENVAR_DCCSI_LOG_PATH -from azpy.constants import PATH_DCCSI_LOG_PATH -_DCCSI_LOG_PATH = Path(os.getenv(ENVAR_DCCSI_LOG_PATH, - Path(PATH_DCCSI_LOG_PATH.format(LY_DEV=_LY_DEV, - LY_PROJECT=_LY_PROJECT)))) -os.environ["DYNACONF_DCCSI_LOG_PATH"] = str(_DCCSI_LOG_PATH) - -# hard checks -if not _LY_BIN_PATH.exists(): - raise Exception('LY_BIN_PATH does NOT exist: {0}'.format(_LY_BIN_PATH)) -else: - # adding to sys.path apparently doesn't work for .dll locations like Qt - os.environ['PATH'] = _LY_BIN_PATH.as_posix() + os.pathsep + os.environ['PATH'] - -_LOGGER.info('Dynaconf config.py ... DONE') +def init_o3de_core(engine_path=_O3DE_DEV, + build_folder=_O3DE_BUILD_FOLDER, + project_name=None, + project_path=_O3DE_PROJECT_PATH): + """Initialize the DCCsi Core dynamic env and settings""" + # `envvar_prefix` = export envvars with `export DYNACONF_FOO=bar`. + # `settings_files` = Load this files in the order. + # here we are modifying or adding to the dynamic config settings on import + settings = Dynaconf(envvar_prefix='DYNACONF', + settings_files=['settings.json', + 'dev.settings.json', + 'user.settings.json', + '.secrets.json']) + + # global settings + os.environ["DYNACONF_DCCSI_OS_FOLDER"] = azpy.config_utils.get_os() + os.environ["DYNACONF_DCCSI_GDEBUG"] = str(_DCCSI_GDEBUG) + os.environ["DYNACONF_DCCSI_DEV_MODE"] = str(_DCCSI_DEV_MODE) + os.environ['DYNACONF_DCCSI_LOGLEVEL'] = str(_DCCSI_LOGLEVEL) + + os.environ["DYNACONF_DCCSI_PATH"] = str(_DCCSI_PATH.resolve()) + os.environ['PATH'] = _DCCSI_PATH.as_posix() + os.pathsep + os.environ['PATH'] + + # we already defaulted to discovering these two early because of importance + #os.environ["DYNACONF_O3DE_DEV"] = str(_O3DE_DEV.resolve()) + #os.environ["DYNACONF_O3DE_PROJECT_PATH"] = str(_O3DE_PROJECT_PATH) + # we also already added them to DYNACONF_ + # this in an explicit pass in + if project_path: + _project_path = Path(project_path) + try: + _project_path.exists() + _O3DE_PROJECT_PATH = _project_path + os.environ["DYNACONF_O3DE_PROJECT_PATH"] = str(_O3DE_PROJECT_PATH.resolve()) + except FileExistsError as e: + _LOGGER.error('~ The project path specified does not appear to exist!') + _LOGGER.warning('~ project_path: {}'.format(project_path)) + _LOGGER.warning('~ fallback to engine root: {}'.format()) + project_path = _O3DE_DEV + os.environ["DYNACONF_O3DE_PROJECT_PATH"] = str(_O3DE_DEV.resolve()) + + # we can pull the O3DE_PROJECT (name) from the project path + if not project_name: + project_name = Path(_O3DE_PROJECT_PATH).name + os.environ["DYNACONF_O3DE_PROJECT"] = str(project_name) + # To Do: there might be a project namespace in the project.json? + + # -- O3DE build -- set up \bin\path (for Qt dll access) + os.environ["DYNACONF_O3DE_BUILD_FOLDER"] = str(build_folder) + _O3DE_BUILD_PATH = Path(_O3DE_DEV, build_folder) + + os.environ["DYNACONF_O3DE_BUILD_PATH"] = str(_O3DE_BUILD_PATH.resolve()) + + _O3DE_BIN_PATH = Path(STR_O3DE_BIN_PATH.format(_O3DE_BUILD_PATH)) + os.environ["DYNACONF_O3DE_BIN_PATH"] = str(_O3DE_BIN_PATH.resolve()) + + # hard check + if not _O3DE_BIN_PATH.exists(): + raise Exception('O3DE_BIN_PATH does NOT exist: {0}'.format(_O3DE_BIN_PATH)) + else: + # adding to sys.path apparently doesn't work for .dll locations like Qt + os.environ['PATH'] = _O3DE_BIN_PATH.as_posix() + os.pathsep + os.environ['PATH'] + # -- + + from azpy.constants import TAG_DIR_DCCSI_TOOLS + _DCCSI_TOOLS_PATH = Path(_DCCSI_PATH, TAG_DIR_DCCSI_TOOLS) + os.environ["DYNACONF_DCCSI_TOOLS_PATH"] = str(_DCCSI_TOOLS_PATH.resolve()) + + from azpy.constants import TAG_DCCSI_NICKNAME + from azpy.constants import PATH_DCCSI_LOG_PATH + _DCCSI_LOG_PATH = Path(PATH_DCCSI_LOG_PATH.format(O3DE_PROJECT_PATH=project_path, + TAG_DCCSI_NICKNAME=TAG_DCCSI_NICKNAME)) + os.environ["DYNACONF_DCCSI_LOG_PATH"] = str(_DCCSI_LOG_PATH) + + from azpy.constants import TAG_DIR_REGISTRY, TAG_DCCSI_CONFIG + _DCCSI_CONFIG_PATH = Path(project_path, TAG_DIR_REGISTRY, TAG_DCCSI_CONFIG) + os.environ["DYNACONF_DCCSI_CONFIG_PATH"] = str(_DCCSI_CONFIG_PATH.resolve()) + + from azpy.constants import TAG_DIR_DCCSI_TOOLS + _DCCSIG_TOOLS_PATH = Path.joinpath(_DCCSI_PATH, TAG_DIR_DCCSI_TOOLS) + os.environ["DYNACONF_DCCSIG_TOOLS_PATH"] = str(_DCCSIG_TOOLS_PATH.resolve()) + + _O3DE_DCCSI_PATH = os.environ['PATH'] + os.environ["DYNACONF_PATH"] = _O3DE_DCCSI_PATH + + from dynaconf import settings + + _LOGGER.info('~ config.init_o3de_core() ... DONE') + + return settings # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -# settings.setenv() # doing this will add the additional DYNACONF_ envars -def get_config_settings(setup_ly_pyside=False): - """Convenience method to set and retreive settings directly from module.""" - from dynaconf import settings +def init_o3de_python(engine_path=_O3DE_DEV, + engine_bin=_O3DE_BIN_PATH, + dccsi_path=_DCCSI_PATH): + + # pathify + _O3DE_DEV = Path(engine_path) + _O3DE_BIN_PATH = Path(engine_bin) + _DCCSI_PATH = Path(dccsi_path) + + # python config + _DCCSI_PYTHON_PATH = Path(_DCCSI_PATH,'3rdParty','Python') + os.environ["DYNACONF_DCCSI_PYTHON_PATH"] = str(_DCCSI_PYTHON_PATH.resolve()) + + _DCCSI_PYTHON_LIB_PATH = azpy.config_utils.bootstrap_dccsi_py_libs(_DCCSI_PATH) + os.environ["DYNACONF_DCCSI_PYTHON_LIB_PATH"] = str(_DCCSI_PYTHON_LIB_PATH.resolve()) + os.environ['PATH'] = _DCCSI_PYTHON_LIB_PATH.as_posix() + os.pathsep + os.environ['PATH'] + site.addsitedir(_DCCSI_PYTHON_LIB_PATH) + _O3DE_PYTHONPATH.append(_DCCSI_PYTHON_LIB_PATH.resolve()) + + site.addsitedir(_O3DE_BIN_PATH) + _O3DE_PYTHONPATH.append(_O3DE_BIN_PATH.resolve()) + + _O3DE_PY_EXE = Path(sys.executable) + _DCCSI_PY_IDE = Path(_O3DE_PY_EXE) + os.environ["DYNACONF_DCCSI_PY_IDE"] = str(_DCCSI_PY_IDE.resolve()) + + _O3DE_PYTHONHOME = Path(_O3DE_PY_EXE.parents[0]) + os.environ["DYNACONF_O3DE_PYTHONHOME"] = str(_O3DE_PYTHONHOME.resolve()) + os.environ['PATH'] = _O3DE_PYTHONHOME.as_posix() + os.pathsep + os.environ['PATH'] + _LOGGER.info('~ O3DE_PYTHONHOME - is now the folder containing O3DE python executable') + + _O3DE_PYTHON_INSTALL = Path(_O3DE_DEV, 'python') + os.environ["DYNACONF_O3DE_PYTHON_INSTALL"] = str(_O3DE_PYTHON_INSTALL.resolve()) + os.environ['PATH'] = _O3DE_PYTHON_INSTALL.as_posix() + os.pathsep + os.environ['PATH'] + + if sys.platform.startswith('win'): + _DCCSI_PY_BASE = Path(_O3DE_PYTHON_INSTALL, 'python.cmd') + elif sys.platform == "linux": + _DCCSI_PY_BASE = Path(_O3DE_PYTHON_INSTALL, 'python.sh') + elif sys.platform == "darwin": + _DCCSI_PY_BASE = Path(_O3DE_PYTHON_INSTALL, 'python.sh') + else: + _DCCSI_PY_BASE = None + + if _DCCSI_PY_BASE: + os.environ["DYNACONF_DCCSI_PY_BASE"] = str(_DCCSI_PY_BASE.resolve()) + + _O3DE_DCCSI_PATH = os.environ['PATH'] + os.environ["DYNACONF_PATH"] = _O3DE_DCCSI_PATH + + try: + _DCCSI_PYTHONPATH = os.environ['PYTHONPATH'] + os.environ["DYNACONF_PYTHONPATH"] = _DCCSI_PYTHONPATH + except: + pass + + from dynaconf import settings + + _LOGGER.info('~ config.init_o3de_python() ... DONE') + + return settings +# ------------------------------------------------------------------------- - if setup_ly_pyside: - init_ly_pyside(settings.LY_DEV) - settings.setenv() +# ------------------------------------------------------------------------- +# settings.setenv() # doing this will add the additional DYNACONF_ envars +def get_config_settings(engine_path=_O3DE_DEV, + build_folder=_O3DE_BUILD_FOLDER, + project_name=None, + project_path=_O3DE_PROJECT_PATH, + enable_o3de_python=None, + enable_o3de_pyside2=None, + set_env=True): + """Convenience method to initialize and retreive settings directly from module.""" + + settings = init_o3de_core(engine_path, + build_folder, + project_name, + project_path) + + if enable_o3de_python: + settings = init_o3de_python(settings.O3DE_DEV, + settings.O3DE_BIN_PATH, + settings.DCCSI_PATH) + + # These should ONLY be set for O3DE and non-DCC environments + # They will most likely cause other Qt/PySide DCC apps to fail + # or hopefully they can be overridden for DCC envionments + # that provide their own Qt dlls and Pyside2 + # _LOGGER.info('QTFORPYTHON_PATH: {}'.format(settings.QTFORPYTHON_PATH)) + # _LOGGER.info('QT_PLUGIN_PATH: {}'.format(settings.QT_PLUGIN_PATH)) + # assume our standalone python tools wants this access? + # it's safe to do this for dev and from ide + if enable_o3de_pyside2: + settings = init_o3de_pyside2(settings.DCCSI_PATH, + settings.O3DE_BIN_PATH) + + # now standalone we can validate the config. env, settings. + from dynaconf import settings + if set_env: + settings.setenv() return settings # --- END ----------------------------------------------------------------- @@ -274,54 +489,216 @@ def get_config_settings(setup_ly_pyside=False): # Main Code Block, runs this script as main (testing) # ------------------------------------------------------------------------- if __name__ == '__main__': - """Run this file as main""" - - _LOG_LEVEL = int(10) # same as _logging.DEBUG - - _LOGGER = azpy.initialize_logger(_PACKAGENAME, - log_to_file=True, - default_log_level=_LOG_LEVEL) - - from dynaconf import settings - + """Run this file as a standalone cli script""" + + _MODULENAME = __name__ + if _MODULENAME is '__main__': + _MODULENAME = 'DCCsi.config' + + from azpy.constants import STR_CROSSBAR + + while 0: # temp internal debug flag + _DCCSI_GDEBUG = True + break + + # overide logger for standalone to be more verbose and log to file + _LOGGER = azpy.initialize_logger(_MODULENAME, + log_to_file=_DCCSI_GDEBUG, + default_log_level=_DCCSI_LOGLEVEL) + + # happy print + _LOGGER.info(STR_CROSSBAR) + _LOGGER.info('~ constants.py ... Running script as __main__') + _LOGGER.info(STR_CROSSBAR) + + # go ahead and run the rest of the configuration + # parse the command line args + import argparse + parser = argparse.ArgumentParser( + description='O3DE DCCsi Dynamic Config (dynaconf)', + epilog="Attempts to determine O3DE project if -pp not set") + parser.add_argument('-gd', '--global-debug', + type=bool, + required=False, + help='Enables global debug flag.') + parser.add_argument('-dm', '--developer-mode', + type=bool, + required=False, + help='Enables dev mode for early auto attaching debugger.') + parser.add_argument('-ep', '--engine-path', + type=pathlib.Path, + required=False, + help='The path to the o3de engine.') + parser.add_argument('-bf', '--build-folder', + type=str, + required=False, + help='The name (tag) of the o3de build folder, example build or windows_vs2019.') + parser.add_argument('-pp', '--project-path', + type=pathlib.Path, + required=False, + help='The path to the project.') + parser.add_argument('-pn', '--project-name', + type=str, + required=False, + help='The name of the project.') + parser.add_argument('-py', '--enable-python', + type=bool, + required=False, + help='Enables O3DE python access.') + parser.add_argument('-qt', '--enable-qt', + type=bool, + required=False, + help='Enables O3DE Qt\PySide2 access.') + parser.add_argument('-sd', '--set-debugger', + type=str, + required=False, + help='Default debugger: WING, others: PYCHARM, VSCODE (not yet implemented).') + parser.add_argument('-pc', '--project-config', + type=bool, + required=False, + help='Enables reading the projects registry\dccsiconfiguration.setreg.') + parser.add_argument('-es', '--export-settings', + type=pathlib.Path, + required=False, + help='Writes managed settings to specified path.') + parser.add_argument('-ec', '--export-configuration', + type=bool, + required=False, + help='writes settings as a O3DE registry\dccsiconfiguration.setreg.') + parser.add_argument('-tp', '--test-pyside2', + type=bool, + required=False, + help='Runs Qt/PySide2 tests and reports.') + args = parser.parse_args() + + # easy overrides + if args.global_debug: + _DCCSI_GDEBUG = True + os.environ["DYNACONF_DCCSI_GDEBUG"] = str(_DCCSI_GDEBUG) + if args.developer_mode: + attach_debugger() # attempts to start debugger + + if args.set_debugger: + _LOGGER.info('Setting and switching debugger type from WingIDE not implemented.') + # To Do: implement debugger plugin pattern + + # need to do a little plumbing + if not args.engine_path: + args.engine_path=_O3DE_DEV + if not args.build_folder: + from azpy.constants import TAG_DIR_O3DE_BUILD_FOLDER + args.build_folder = TAG_DIR_O3DE_BUILD_FOLDER + if not args.project_path: + args.project_path=_O3DE_PROJECT_PATH + + if _DCCSI_GDEBUG: + args.enable_python = True + args.enable_qt = True + + # now standalone we can validate the config. env, settings. + settings = get_config_settings(engine_path=args.engine_path, + build_folder=args.build_folder, + project_name=args.project_name, + project_path=args.project_path, + enable_o3de_python=args.enable_python, + enable_o3de_pyside2=args.enable_qt) + + ## CORE + _LOGGER.info(STR_CROSSBAR) # not using fstrings in this module because it might run in py2.7 (maya) _LOGGER.info('DCCSI_GDEBUG: {}'.format(settings.DCCSI_GDEBUG)) _LOGGER.info('DCCSI_DEV_MODE: {}'.format(settings.DCCSI_DEV_MODE)) _LOGGER.info('DCCSI_LOGLEVEL: {}'.format(settings.DCCSI_LOGLEVEL)) - - _LOGGER.info('OS_FOLDER: {}'.format(settings.OS_FOLDER)) - _LOGGER.info('LY_PROJECT: {}'.format(settings.LY_PROJECT)) - _LOGGER.info('LY_PROJECT_PATH: {}'.format(settings.LY_PROJECT_PATH)) - _LOGGER.info('LY_DEV: {}'.format(settings.LY_DEV)) - _LOGGER.info('LY_BUILD_PATH: {}'.format(settings.LY_BUILD_PATH)) - _LOGGER.info('LY_BIN_PATH: {}'.format(settings.LY_BIN_PATH)) - + _LOGGER.info('DCCSI_OS_FOLDER: {}'.format(settings.DCCSI_OS_FOLDER)) + + _LOGGER.info('O3DE_DEV: {}'.format(settings.O3DE_DEV)) + _LOGGER.info('O3DE_O3DE_BUILD_FOLDER: {}'.format(settings.O3DE_BUILD_PATH)) + _LOGGER.info('O3DE_BUILD_PATH: {}'.format(settings.O3DE_BUILD_PATH)) + _LOGGER.info('O3DE_BIN_PATH: {}'.format(settings.O3DE_BIN_PATH)) + + _LOGGER.info('O3DE_PROJECT: {}'.format(settings.O3DE_PROJECT)) + _LOGGER.info('O3DE_PROJECT_PATH: {}'.format(settings.O3DE_PROJECT_PATH)) + + _LOGGER.info('DCCSI_PATH: {}'.format(settings.DCCSI_PATH)) _LOGGER.info('DCCSI_LOG_PATH: {}'.format(settings.DCCSI_LOG_PATH)) - _LOGGER.info('DCCSI_CONFIG_PATH: {}'.format(settings.DCCSI_CONFIG_PATH)) - _LOGGER.info('DCCSIG_PATH: {}'.format(settings.DCCSIG_PATH)) - _LOGGER.info('DCCSI_PYTHON_LIB_PATH: {}'.format(settings.DCCSI_PYTHON_LIB_PATH)) - _LOGGER.info('DDCCSI_PY_BASE: {}'.format(settings.DDCCSI_PY_BASE)) - - # To Do: These should ONLY be set for Lumberyard and non-DCC environments - # They will most likely cause Qt/PySide DCC apps to fail - # or hopefully they can be overridden for DCC envionments - # that provide their own Qt dlls and Pyside2 - #_LOGGER.info('QTFORPYTHON_PATH: {}'.format(settings.QTFORPYTHON_PATH)) - #_LOGGER.info('QT_PLUGIN_PATH: {}'.format(settings.QT_PLUGIN_PATH)) - - init_ly_pyside(settings.LY_DEV) # init lumberyard Qt/PySide2 - # from dynaconf import settings # <-- no need to reimport + + if settings.O3DE_DCCSI_ENV_TEST: + _LOGGER.info('O3DE_DCCSI_ENV_TEST: {}'.format(settings.O3DE_DCCSI_ENV_TEST)) + + _LOGGER.info(STR_CROSSBAR) + _LOGGER.info('') + + if args.enable_python: + _LOGGER.info(STR_CROSSBAR) + _LOGGER.info('DCCSI_PYTHON_PATH'.format(settings.DCCSI_PYTHON_PATH)) + _LOGGER.info('DCCSI_PYTHON_LIB_PATH: {}'.format(settings.DCCSI_PYTHON_LIB_PATH)) + _LOGGER.info('DCCSI_PY_IDE'.format(settings.DCCSI_PY_IDE)) + _LOGGER.info('O3DE_PYTHONHOME'.format(settings.O3DE_PYTHONHOME)) + _LOGGER.info('O3DE_PYTHON_INSTALL'.format(settings.O3DE_PYTHON_INSTALL)) + _LOGGER.info('DCCSI_PY_BASE: {}'.format(settings.DCCSI_PY_BASE)) + _LOGGER.info(STR_CROSSBAR) + _LOGGER.info('') + else: + _LOGGER.info('Tip: add arg --enable-python to extend the environment with O3DE python access') + + if args.enable_qt: + _LOGGER.info(STR_CROSSBAR) + # _LOGGER.info('QTFORPYTHON_PATH: {}'.format(settings.QTFORPYTHON_PATH)) + _LOGGER.info('QT_PLUGIN_PATH: {}'.format(settings.QT_PLUGIN_PATH)) + _LOGGER.info('QT_QPA_PLATFORM_PLUGIN_PATH: {}'.format(settings.QT_QPA_PLATFORM_PLUGIN_PATH)) + _LOGGER.info('DCCSI_PYSIDE2_TOOLS: {}'.format(settings.DCCSI_PYSIDE2_TOOLS)) + _LOGGER.info(STR_CROSSBAR) + _LOGGER.info('') + else: + _LOGGER.info('Tip: add arg --enable-qt to extend the environment with O3DE Qt/PySide2 support') + settings.setenv() # doing this will add/set the additional DYNACONF_ envars - - #_LOGGER.info('QTFORPYTHON_PATH: {}'.format(settings.QTFORPYTHON_PATH)) - _LOGGER.info('LY_BIN_PATH: {}'.format(settings.LY_BIN_PATH)) - _LOGGER.info('QT_PLUGIN_PATH: {}'.format(settings.QT_PLUGIN_PATH)) - _LOGGER.info('QT_QPA_PLATFORM_PLUGIN_PATH: {}'.format(settings.QT_QPA_PLATFORM_PLUGIN_PATH)) - _LOGGER.info('DCCSI_PYSIDE2_TOOLS: {}'.format(settings.DCCSI_PYSIDE2_TOOLS)) - - test_pyside2() # test PySide2 access with a pop-up button + + if _DCCSI_GDEBUG or args.export_settings: + # to do: need to add malformed json validation to \settings.json + # this can cause a bad error that is deep and hard to debug + + # writting settings + from dynaconf import loaders + from dynaconf.utils.boxing import DynaBox + + _settings_dict = settings.as_dict() + + # default temp filename + _settings_file = Path('settings_export.json.tmp') + + # writes to a file, the format is inferred by extension + # can be .yaml, .toml, .ini, .json, .py + #loaders.write(_settings_file, DynaBox(data).to_dict(), merge=False, env='development') + #loaders.write(_settings_file, DynaBox(data).to_dict()) + + # we want to possibly modify or stash our settings into a o3de .setreg + from box import Box + _settings_box = Box(_settings_dict) + + _LOGGER.info('Pretty print, _settings_box: {}'.format(_settings_file)) + _LOGGER.info(str(_settings_box.to_json(sort_keys=True, + indent=4))) + + # writes settings box + _settings_box.to_json(filename=_settings_file.as_posix(), + sort_keys=True, + indent=4) + + if _DCCSI_GDEBUG or args.test_pyside2: + test_pyside2() # test PySide2 access with a pop-up button + try: + import pyside2uic + except ImportError as e: + _LOGGER.warning("Could not import 'pyside2uic'") + _LOGGER.warning("Refer to: '< local DCCsi >\3rdParty\Python\README.txt'") + _LOGGER.error(e) + + # return + sys.exit() +# --- END ----------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/settings.json b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/settings.json index 0967ef424b..718c6110c3 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/settings.json +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/settings.json @@ -1 +1,13 @@ -{} +{ + "LOAD_DOTENV": true, + "COMPANY": "Amazon", + "DCCSI_GDEBUG": "False", + "DCCSI_DEV_MODE": "False", + "DCCSI_GDEBUGGER": "WING", + "DCCSI_LOGLEVEL": 20, + "DEFAULT_SETTINGS_PATHS": [ + "settings.py", + "settings.json", + ".secrets.json" + ] +} \ No newline at end of file diff --git a/Gems/AtomTressFX/Assets/Passes/AtomTressFX_MainPipeline.pass b/Gems/AtomTressFX/Assets/Passes/AtomTressFX_MainPipeline.pass index 84bb85c3e1..9bfa746bf1 100644 --- a/Gems/AtomTressFX/Assets/Passes/AtomTressFX_MainPipeline.pass +++ b/Gems/AtomTressFX/Assets/Passes/AtomTressFX_MainPipeline.pass @@ -212,7 +212,11 @@ // instead of regular Depth as DepthStencil. Specifically, HairResolvePPLL.pass and the associated // .azsl file will need to be updated. "Name": "HairParentPass", - "TemplateName": "HairParentPassTemplate", + // Note: The following two lines represent the choice of rendering pipeline for the hair. + // You can either choose to use PPLL or ShortCut and accordingly change the flag + // 'm_usePPLLRenderTechnique' in the class 'HairFeatureProcessor.cpp' +// "TemplateName": "HairParentPassTemplate", + "TemplateName": "HairParentShortCutPassTemplate", "Enabled": true, "Connections": [ // Critical to keep DepthLinear as input - used to set the size of the Head PPLL image buffer. diff --git a/Gems/AtomTressFX/Assets/Passes/AtomTressFX_PassTemplates.azasset b/Gems/AtomTressFX/Assets/Passes/AtomTressFX_PassTemplates.azasset index a287664286..1580a4e1a9 100644 --- a/Gems/AtomTressFX/Assets/Passes/AtomTressFX_PassTemplates.azasset +++ b/Gems/AtomTressFX/Assets/Passes/AtomTressFX_PassTemplates.azasset @@ -8,6 +8,11 @@ "Name": "HairParentPassTemplate", "Path": "Passes/HairParentPass.pass" }, + { + "Name": "HairParentShortCutPassTemplate", + "Path": "Passes/HairParentShortCutPass.pass" + }, + { "Name": "HairGlobalShapeConstraintsComputePassTemplate", "Path": "Passes/HairGlobalShapeConstraintsCompute.pass" @@ -32,6 +37,7 @@ "Name": "HairUpdateFollowHairComputePassTemplate", "Path": "Passes/HairUpdateFollowHairCompute.pass" }, + { "Name": "HairPPLLRasterPassTemplate", "Path": "Passes/HairFillPPLL.pass" @@ -39,6 +45,23 @@ { "Name": "HairPPLLResolvePassTemplate", "Path": "Passes/HairResolvePPLL.pass" + }, + + { + "Name": "HairShortCutGeometryDepthAlphaPassTemplate", + "Path": "Passes/HairShortCutGeometryDepthAlpha.pass" + }, + { + "Name": "HairShortCutResolveDepthPassTemplate", + "Path": "Passes/HairShortCutResolveDepth.pass" + }, + { + "Name": "HairShortCutGeometryShadingPassTemplate", + "Path": "Passes/HairShortCutGeometryShading.pass" + }, + { + "Name": "HairShortCutResolveColorPassTemplate", + "Path": "Passes/HairShortCutResolveColor.pass" } ] } diff --git a/Gems/AtomTressFX/Assets/Passes/HairParentPass.pass b/Gems/AtomTressFX/Assets/Passes/HairParentPass.pass index 6ae8b9526e..c8c90e24f7 100644 --- a/Gems/AtomTressFX/Assets/Passes/HairParentPass.pass +++ b/Gems/AtomTressFX/Assets/Passes/HairParentPass.pass @@ -71,6 +71,12 @@ } } ], + "FallbackConnections": [ + { + "Input": "DepthLinearInput", + "Output": "DepthLinear" + } + ], "PassRequests": [ { "Name": "HairGlobalShapeConstraintsComputePass", diff --git a/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass b/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass new file mode 100644 index 0000000000..83f9f0d432 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass @@ -0,0 +1,400 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairParentShortCutPassTemplate", + "PassClass": "ParentPass", + "Slots": [ + { + "Name": "RenderTargetInputOutput", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "RenderTarget" + }, + { + // used for copy from MSAA to regular RT + "Name": "RenderTargetInputOnly", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + // This is the depth stencil buffer that is to be used by the fill pass + // to early reject pixels by depth and in the resolve pass to write the + // the hair depth + { + "Name": "Depth", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "DepthStencil" + }, + // Keep DepthLinear as input - used to set the size of the Head PPLL image buffer. + // If DepthLinear is not availbale - connect to another viewport (non MSAA) image. + { + "Name": "DepthLinearInput", + "SlotType": "Input" + }, + { + "Name": "DepthLinear", + "SlotType": "Output" + }, + + // Lights & Shadows resources + { + "Name": "DirectionalShadowmap", + "SlotType": "Input" + }, + { + "Name": "DirectionalESM", + "SlotType": "Input" + }, + { + "Name": "ProjectedShadowmap", + "SlotType": "Input" + }, + { + "Name": "ProjectedESM", + "SlotType": "Input" + }, + { + "Name": "TileLightData", + "SlotType": "Input" + }, + { + "Name": "LightListRemapped", + "SlotType": "Input" + } + ], + "Connections": [ + { + "LocalSlot": "DepthLinear", + "AttachmentRef": { + "Pass": "DepthToDepthLinearPass", + "Attachment": "Output" + } + } + ], + "FallbackConnections": [ + { + "Input": "DepthLinearInput", + "Output": "DepthLinear" + } + ], + "PassRequests": [ + { + "Name": "HairGlobalShapeConstraintsComputePass", + "TemplateName": "HairGlobalShapeConstraintsComputePassTemplate", + "Enabled": true + }, + { + "Name": "HairCalculateStrandLevelDataComputePass", + "TemplateName": "HairCalculateStrandLevelDataComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairGlobalShapeConstraintsComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + { + "Name": "HairVelocityShockPropagationComputePass", + "TemplateName": "HairVelocityShockPropagationComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairCalculateStrandLevelDataComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + { + "Name": "HairLocalShapeConstraintsComputePass", + "TemplateName": "HairLocalShapeConstraintsComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairVelocityShockPropagationComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + { + "Name": "HairLengthConstraintsWindAndCollisionComputePass", + "TemplateName": "HairLengthConstraintsWindAndCollisionComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairLocalShapeConstraintsComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + { + "Name": "HairUpdateFollowHairComputePass", + "TemplateName": "HairUpdateFollowHairComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairLengthConstraintsWindAndCollisionComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + + // Render Target Copy from MS to Regular + { + "Name": "RenderTargetCopyPass", + "TemplateName": "FullscreenCopyTemplate", + "Connections": [ + { + "LocalSlot": "Input", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "RenderTargetInputOnly" + } + }, + { + "LocalSlot": "Output", + "AttachmentRef": { + "Pass": "This", + "Attachment": "Output" + } + } + ], + "ImageAttachments": [ + { + "Name": "Output", + "SizeSource": { + "Source": { + "Pass": "This", + "Attachment": "Input" + } + }, + "FormatSource": { + "Pass": "This", + "Attachment": "Input" + }, + "GenerateFullMipChain": false + } + ] + }, + + // Rendering Passes + { + "Name": "HairShortCutGeometryDepthAlphaPass", + "TemplateName": "HairShortCutGeometryDepthAlphaPassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairUpdateFollowHairComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + }, + { + "LocalSlot": "Depth", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "Depth" + } + }, + { + "LocalSlot": "InverseAlphaRTOutput", + "AttachmentRef": { + "Pass": "This", + "Attachment": "InverseAlphaRTOutput" + } + }, + { + "LocalSlot": "HairDepthsTextureArray", + "AttachmentRef": { + "Pass": "This", + "Attachment": "HairDepthsTextureArray" + } + } + ] + }, + + { + "Name": "HairShortCutResolveDepthPass", + "TemplateName": "HairShortCutResolveDepthPassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "Depth", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "Depth" + } + }, + { + "LocalSlot": "HairDepthsTextureArray", + "AttachmentRef": { + "Pass": "HairShortCutGeometryDepthAlphaPass", + "Attachment": "HairDepthsTextureArray" + } + } + ] + }, + + { + "Name": "HairShortCutGeometryShadingPass", + "TemplateName": "HairShortCutGeometryShadingPassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "HairColorRenderTarget", + "AttachmentRef": { + "Pass": "This", + "Attachment": "HairColorRenderTarget" + } + }, + { + // The final render target - this is MSAA mode RT - would it be cheaper to + // use non-MSAA and then copy? + "LocalSlot": "RenderTargetInputOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "RenderTargetInputOutput" + } + }, + { + "LocalSlot": "DepthLinear", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "DepthLinearInput" + } + }, + { + "LocalSlot": "Depth", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "Depth" + } + }, + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairUpdateFollowHairComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + }, + + // Shadows resources + { + "LocalSlot": "DirectionalShadowmap", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "DirectionalShadowmap" + } + }, + { + "LocalSlot": "DirectionalESM", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "DirectionalESM" + } + }, + { + "LocalSlot": "ProjectedShadowmap", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "ProjectedShadowmap" + } + }, + { + "LocalSlot": "ProjectedESM", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "ProjectedESM" + } + }, + + // Lights Resources + { + "LocalSlot": "TileLightData", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "TileLightData" + } + }, + { + "LocalSlot": "LightListRemapped", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "LightListRemapped" + } + } + ] + }, + + { + "Name": "HairShortCutResolveColorPass", + "TemplateName": "HairShortCutResolveColorPassTemplate", + "Enabled": true, + "Connections": [ + { + // The final render target - this is MSAA mode RT - would it be cheaper to + // use non-MSAA and then copy? + "LocalSlot": "RenderTargetInputOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "RenderTargetInputOutput" + } + }, + { + "LocalSlot": "AccumulatedInverseAlpha", + "AttachmentRef": { + "Pass": "HairShortCutGeometryDepthAlphaPass", + "Attachment": "InverseAlphaRTOutput" + } + }, + { + "LocalSlot": "HairColorTexture", + "AttachmentRef": { + "Pass": "HairShortCutGeometryShadingPass", + "Attachment": "HairColorRenderTarget" + } + } + ] + }, + + { + // This pass copies the updated depth buffer (now contains hair depth) to linear depth texture + // for downstream passes to use. This can be optimized even further by writing into the stencil + // buffer pixels that were touched by HairPPLLResolvePass hence preventing depth update unless + // it is hair. + "Name": "DepthToDepthLinearPass", + "TemplateName": "DepthToLinearTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "Input", + "AttachmentRef": { + "Pass": "HairShortCutResolveDepthPass", + "Attachment": "Depth" + } + } + ] + } + + ] + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryDepthAlpha.pass b/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryDepthAlpha.pass new file mode 100644 index 0000000000..559224faa6 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryDepthAlpha.pass @@ -0,0 +1,101 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairShortCutGeometryDepthAlphaPassTemplate", + "PassClass": "HairShortCutGeometryDepthAlphaPass", + "Slots": [ + { + "Name": "SkinnedHairSharedBuffer", + "ShaderInputName": "m_skinnedHairSharedBuffer", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { // DepthStencil for early disqualifying the pixel based on depth. No write. + "Name": "Depth", + "SlotType": "Input", + "ScopeAttachmentUsage": "DepthStencil" + }, + { + // the regular render target is blended using inverse alpha to reduce the + // incoming color contribution based on the hair thickness and alpha. + "Name": "InverseAlphaRTOutput", + "SlotType": "Output", + "ScopeAttachmentUsage": "RenderTarget", + "LoadStoreAction": { + "LoadAction": "Clear", + "ClearValue": { + "Value": [ 1.0, 1.0, 1.0, 1.0 ] + }, + "StoreAction": "Store" + } + }, + { + "Name": "HairDepthsTextureArray", + "SlotType": "Output", + "ScopeAttachmentUsage": "Shader", + "ShaderInputName": "m_RWFragmentDepthsTexture", + "LoadStoreAction": { + "LoadAction": "Clear", + "ClearValue": { // reverse depth order: closer --> 1.0 + "Value": [ 0.0, 0.0, 0.0, 0.0 ] + }, + "StoreAction": "Store" + } + } + ], + "ImageAttachments": [ + { + // This buffer is used as the render target and should be at non-MSAA screen resolution + // to make sure no overwork is done. + "Name": "InverseAlphaRTOutput", + "SizeSource": { + "Source": { + "Pass": "Parent", + "Attachment": "DepthLinear" + } + }, + "ImageDescriptor": { + "Format": "R32_FLOAT", + "SharedQueueMask": "Graphics", + "BindFlags": [ + "Color", + "ShaderRead" + ] + } + }, + { + "Name": "HairDepthsTextureArray", + "SizeSource": { + "Source": { + "Pass": "Parent", + "Attachment": "DepthLinear" + } + }, + "ImageDescriptor": { + "Format": "R32_UINT", + "ArraySize": "3", + "SharedQueueMask": "Graphics", + "BindFlags": [ + "ShaderReadWrite", + "ShaderWrite", + "ShaderRead" + ] + } + } + ], + "PassData": { + "$type": "RasterPassData", + "DrawListTag": "HairGeometryDepthAlphaDrawList", + "PipelineViewTag": "MainCamera", + "PassSrgShaderAsset": { + // Looking for it in the Shaders directory relative to the Assets directory + "FilePath": "Shaders/HairShortCutGeometryDepthAlpha.shader" + } + } + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryShading.pass b/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryShading.pass new file mode 100644 index 0000000000..5940f8c549 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryShading.pass @@ -0,0 +1,155 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairShortCutGeometryShadingPassTemplate", + "PassClass": "HairShortCutGeometryShadingPass", + "Slots": [ + + { // Temporary color buffer to store the gathered shaded hair color - MSAA + "Name": "HairColorRenderTarget", + "SlotType": "Output", + "ScopeAttachmentUsage": "RenderTarget", + "LoadStoreAction": { + "LoadAction": "Clear", + "ClearValue": { // reverse depth order: closer --> 1.0 + "Value": [ 0.0, 0.0, 0.0, 0.0 ] + }, + "StoreAction": "Store" + } + }, + + { + // This RT is MSAA - is it cheaper to avoid doing this work and only do a copy at a separate pass? + "Name": "RenderTargetInputOutput", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { // Used to get the transform from screen space to world space. + "Name": "DepthLinear", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { // For comparing the depth to early disqualify but not to write + "Name": "Depth", + "SlotType": "Input", + "ScopeAttachmentUsage": "DepthStencil" + }, + { + "Name": "SkinnedHairSharedBuffer", + "ShaderInputName": "m_skinnedHairSharedBuffer", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + + //------------- Shadowing Resources ------------- + { + "Name": "DirectionalShadowmap", + "ShaderInputName": "m_directionalLightShadowmap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + { + "Name": "DirectionalESM", + "ShaderInputName": "m_directionalLightExponentialShadowmap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + { + "Name": "ProjectedShadowmap", + "ShaderInputName": "m_projectedShadowmaps", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + { + "Name": "ProjectedESM", + "ShaderInputName": "m_projectedExponentialShadowmap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + + //------------- Lighting Resources ------------- + { + "Name": "BRDFTextureInput", + "ShaderInputName": "m_brdfMap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { + "Name": "TileLightData", + "SlotType": "Input", + "ShaderInputName": "m_tileLightData", + "ScopeAttachmentUsage": "Shader" + }, + { + "Name": "LightListRemapped", + "SlotType": "Input", + "ShaderInputName": "m_lightListRemapped", + "ScopeAttachmentUsage": "Shader" + } + + ], + "ImageAttachments": [ + { + // The shader hair color render target - important to have at a non-MSAA mode + // so that no overwork is done on sampling. + "Name": "HairColorRenderTarget", + "SizeSource": { + "Source": { + "Pass": "Parent", + "Attachment": "RenderTargetInputOutput" + } + }, + "ImageDescriptor": { + "Format": "R16G16B16A16_FLOAT", + "SharedQueueMask": "Graphics", + "BindFlags": [ + "Color", + "ShaderRead" + ] + } + }, + { + "Name": "BRDFTexture", + "Lifetime": "Imported", + "AssetRef": { + "FilePath": "Textures/BRDFTexture.attimage" + } + } + ], + "Connections": [ + { + "LocalSlot": "BRDFTextureInput", + "AttachmentRef": { + "Pass": "This", + "Attachment": "BRDFTexture" + } + } + ], + "PassData": { + "$type": "RasterPassData", + "DrawListTag": "HairGeometryShadingDrawList", + "PipelineViewTag": "MainCamera", + "PassSrgShaderAsset": { + // Looking for it in the Shaders directory relative to the Assets directory + "FilePath": "Shaders/HairShortCutGeometryShading.shader" + } + } + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveColor.pass b/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveColor.pass new file mode 100644 index 0000000000..efbf993307 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveColor.pass @@ -0,0 +1,45 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairShortCutResolveColorPassTemplate", + "PassClass": "FullScreenTriangle", + "Slots": [ + { + // This RT is MSAA - is it cheaper to avoid doing this work and only do a copy at a separate pass? + "Name": "RenderTargetInputOutput", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "RenderTarget", + "LoadStoreAction": { + "LoadAction": "Load", + "StoreAction": "Store" + } + }, + { + "Name": "HairColorTexture", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderInputName": "m_hairColorTexture" + }, + { + "Name": "AccumulatedInverseAlpha", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderInputName": "m_accumInvAlpha" + } + ], + "Connections": [ + ], + "PassData": { + "$type": "FullscreenTrianglePassData", + "ShaderAsset": { + // Looking for it in the Shaders directory relative to the Assets directory + "FilePath": "Shaders/HairShortCutResolveColor.shader" + } + } + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveDepth.pass b/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveDepth.pass new file mode 100644 index 0000000000..021d711f1f --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveDepth.pass @@ -0,0 +1,37 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairShortCutResolveDepthPassTemplate", + "PassClass": "FullScreenTriangle", + "Slots": [ + //------ General Input/Output resources and Render Target ------ + { + "Name": "Depth", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "DepthStencil", + "LoadStoreAction": { + "LoadAction": "Load", + "StoreAction": "Store" + } + }, + { // This holds the K nearset depths. The furthest depth will be taken to be written in the depth buffer. + "Name": "HairDepthsTextureArray", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderInputName": "m_fragmentDepthsTexture" + } + ], + "PassData": { + "$type": "FullscreenTrianglePassData", + "ShaderAsset": { + // Looking for it in the Shaders directory relative to the Assets directory + "FilePath": "Shaders/HairShortCutResolveDepth.shader" + } + } + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Shaders/HairSrgs.azsli b/Gems/AtomTressFX/Assets/Shaders/HairComputeSrgs.azsli similarity index 99% rename from Gems/AtomTressFX/Assets/Shaders/HairSrgs.azsli rename to Gems/AtomTressFX/Assets/Shaders/HairComputeSrgs.azsli index cb0bdc2c6c..a4568588d5 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairSrgs.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairComputeSrgs.azsli @@ -29,7 +29,7 @@ // THE SOFTWARE. // //------------------------------------------------------------------------------ -// File: HairSRGs.azsli +// File: HairComputeSrgs.azsli // // Declarations of SRGs used by the hair shaders. //------------------------------------------------------------------------------ diff --git a/Gems/AtomTressFX/Assets/Shaders/HairFullScreenUtils.azsli b/Gems/AtomTressFX/Assets/Shaders/HairFullScreenUtils.azsli new file mode 100644 index 0000000000..7e57e8102c --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairFullScreenUtils.azsli @@ -0,0 +1,60 @@ +/* +* Modifications 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) AND MIT +* +*/ + +#include +#include +#include + +//============================================================================== +// Generate a fullscreen triangle from pipeline provided vertex id +VSOutput FullScreenVS(VSInput input) +{ + VSOutput OUT; + + float4 posTex = GetVertexPositionAndTexCoords(input.m_vertexID); + + OUT.m_texCoord = float2(posTex.z, posTex.w); // [To Do] - test sign of Y based on original code + OUT.m_position = float4(posTex.xy, 0.0, 1.0); + + return OUT; +} + +//============================================================================== +// Given the depth buffer depth of the current pixel and the fragment XY position, +// reconstruct the NDC. +// screenCoords - from 0.. dimension of the screen of the current pixel +// screenTexture - screen buffer texture representing the same resolution we work in +// sDepth - the depth buffer depth at the fragment location +// NDC - Normalized Device Coordinates = warped screen space ( -1.1, -1..1, 0..1 ) +float3 ScreenPosToNDC( Texture2D screenTexture, float2 screenCoords, float depth ) +{ + uint2 dimensions; + screenTexture.GetDimensions(dimensions.x, dimensions.y); + float2 UV = saturate(screenCoords / dimensions.xy); + + float x = UV.x * 2.0f - 1.0f; + float y = (1.0f - UV.y) * 2.0f - 1.0f; + float3 NDC = float3(x, y, depth); + + return NDC; +} + +// Given the depth buffer depth of the current pixel and the fragment XY position, +// reconstruct the world space position +float3 ScreenPosToWorldPos( + Texture2D screenTexture, float2 screenCoords, float depth, + inout float3 screenPosNDC ) +{ + screenPosNDC = ScreenPosToNDC(screenTexture, screenCoords, depth); + float4 projectedPos = float4(screenPosNDC, 1.0f); // warped projected space [0..1] + float4 positionVS = mul(ViewSrg::m_projectionMatrixInverse, projectedPos); + positionVS /= positionVS.w; // notice the normalization factor - crucial! + float4 positionWS = mul(ViewSrg::m_viewMatrixInverse, positionVS); + + return positionWS.xyz; +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairLightTypes.azsli b/Gems/AtomTressFX/Assets/Shaders/HairLightTypes.azsli index 4b0fa654e3..ffabf7c298 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairLightTypes.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairLightTypes.azsli @@ -465,7 +465,7 @@ void ApplyLighting(inout Surface surface, inout LightingData lightingData) const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight; if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount) { - DirectionalLightShadow::GetShadowCoords(shadowIndex, surface.position, lightingData.shadowCoords); + DirectionalLightShadow::GetShadowCoords(shadowIndex, surface.position, surface.vertexNormal, lightingData.shadowCoords); } // Light loops application. diff --git a/Gems/AtomTressFX/Assets/Shaders/HairLighting.azsli b/Gems/AtomTressFX/Assets/Shaders/HairLighting.azsli index dcdcf43243..7beba248f7 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairLighting.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairLighting.azsli @@ -230,7 +230,7 @@ float3 CalculateLighting( return lightingData.diffuseLighting + lightingData.specularLighting; } -float3 TressFXShading(float2 pixelCoord, float depth, float3 vTangentCoverage, float3 baseColor, float thickness, int shaderParamIndex) +float3 TressFXShading(float2 pixelCoord, float depth, float3 tangent, float3 baseColor, float thickness, int shaderParamIndex) { float3 vNDC; // normalized device / screen coordinates: [-1..1, -1..1, 0..1] float3 vPositionWS = ScreenPosToWorldPos(PassSrg::m_linearDepth, pixelCoord, depth, vNDC); @@ -241,9 +241,6 @@ float3 TressFXShading(float2 pixelCoord, float depth, float3 vTangentCoverage, f float3 vViewDirWS = g_vEye - vPositionWS; - // Need to expand the tangent that was compressed to store in the buffer - float3 vTangent = normalize(vTangentCoverage.xyz * 2.f - 1.f); - //---- TressFX original lighting params setting ---- HairShadeParams params; params.m_color = baseColor; @@ -266,11 +263,19 @@ float3 TressFXShading(float2 pixelCoord, float depth, float3 vTangentCoverage, f if (o_hairLightingModel == HairLightingModel::Kajiya) { // This option should be removed and the Kajiya-Kay model should be operated from within // the Atom lighting loop. - accumulatedLight = SimplifiedHairLighting(vTangent, vPositionWS, vViewDirWS, params, vNDC); + accumulatedLight = SimplifiedHairLighting(tangent, vPositionWS, vViewDirWS, params, vNDC); } else { - accumulatedLight = CalculateLighting(screenCoords, vPositionWS, vViewDirWS, vTangent, thickness, params); + accumulatedLight = CalculateLighting(screenCoords, vPositionWS, vViewDirWS, tangent, thickness, params); } return accumulatedLight; } + +float3 TressFXShadingFullScreen(float2 pixelCoord, float depth, float3 compressedTangent, float3 baseColor, float thickness, int shaderParamIndex) +{ + // The tangent that was compressed to store in the PPLL structure + float3 tangent = normalize(compressedTangent.xyz * 2.f - 1.f); + + return TressFXShading(pixelCoord, depth, tangent, baseColor, thickness, shaderParamIndex); +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairLightingEquations.azsli b/Gems/AtomTressFX/Assets/Shaders/HairLightingEquations.azsli index 83ebc04521..4e4245f765 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairLightingEquations.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairLightingEquations.azsli @@ -66,7 +66,7 @@ option bool o_enableAzimuthCoeff = true; float M_R(Surface surface, float Lh, float sinLiPlusSinLr) { float a = 1.0f * surface.cuticleTilt; // Tilt is translate as the mean offset - float b = 0.5 * surface.roughnessA2; // Roughness is used as the standard deviation + float b = 0.5f * surface.roughnessA2; // Roughness is used as the standard deviation // return GaussianNormalized(sinLiPlusSinLr, a, b); // reference return GaussianNormalized(Lh, a, b); @@ -74,8 +74,8 @@ float M_R(Surface surface, float Lh, float sinLiPlusSinLr) float M_TT(Surface surface, float Lh, float sinLiPlusSinLr) { - float a = 1.0 * surface.cuticleTilt; - float b = 0.5 * surface.roughnessA2; + float a = 1.0f * surface.cuticleTilt; + float b = 0.5f * surface.roughnessA2; // return GaussianNormalized(sinLiPlusSinLr, a, b); // reference return GaussianNormalized(Lh, a, b); @@ -83,8 +83,8 @@ float M_TT(Surface surface, float Lh, float sinLiPlusSinLr) float M_TRT(Surface surface, float Lh, float sinLiPlusSinLr) { - float a = 1.5 * surface.cuticleTilt; - float b = 1.0 * surface.roughnessA2; + float a = 1.5f * surface.cuticleTilt; + float b = 1.0f * surface.roughnessA2; // return GaussianNormalized(sinLiPlusSinLr, a, b); // reference return GaussianNormalized(Lh, a, b); diff --git a/Gems/AtomTressFX/Assets/Shaders/HairRenderingFillPPLL.azsl b/Gems/AtomTressFX/Assets/Shaders/HairRenderingFillPPLL.azsl index 82c677faad..02777435f5 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairRenderingFillPPLL.azsl +++ b/Gems/AtomTressFX/Assets/Shaders/HairRenderingFillPPLL.azsl @@ -40,7 +40,8 @@ //! that can change between passes due to the application of skinning, simulation //! and physics affect and is then read by the rendering shaders. ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback -{ //! This shared buffer needs to match the SharedBuffer structure +{ + //! This shared buffer needs to match the SharedBuffer structure //! shared between all draw calls / dispatches for the hair skinning StructuredBuffer m_skinnedHairSharedBuffer; @@ -101,39 +102,8 @@ ShaderResourceGroup HairDynamicDataSrg : SRG_PerObject // space 1 - per instance #define g_GuideHairVertexTangents HairDynamicDataSrg::m_hairVertexTangents //============================================================================== -#include +#include // VS resides here //============================================================================== -//! Hair input structure to Pixel shaders -struct PS_INPUT_HAIR -{ - float4 Position : SV_POSITION; - float4 Tangent : Tangent; - float4 p0p1 : TEXCOORD0; - float4 StrandColor : TEXCOORD1; -}; - -//! Hair Render VS -PS_INPUT_HAIR RenderHairVS(uint vertexId : SV_VertexID) -{ -// uint2 scrSize; -// PassSrg::m_linearDepth.GetDimensions(scrSize.x, scrSize.y); -// TressFXVertex tressfxVert = GetExpandedTressFXVert(vertexId, g_vEye.xyz, float2(scrSize), g_mVP); - - // [To Do] Hair: the above code should replace the existing but requires modifications to - // the function GetExpandedTressFXVert. - // Note that in Atom g_vViewport is aspect ratio and NOT size. - TressFXVertex tressfxVert = GetExpandedTressFXVert(vertexId, g_vEye.xyz, g_vViewport.zw, g_mVP); - - - PS_INPUT_HAIR Output; - - Output.Position = tressfxVert.Position; - Output.Tangent = tressfxVert.Tangent; - Output.p0p1 = tressfxVert.p0p1; - Output.StrandColor = tressfxVert.StrandColor; - - return Output; -} // Allocate a new fragment location in fragment color, depth, and link buffers int AllocateFragment(int2 vScreenAddress) @@ -202,9 +172,9 @@ void PPLLFillPS(PS_INPUT_HAIR input) ////////////////////////////////////////////////////////////////////// // [To Do] Hair: anti aliasing via coverage requires work and is disabled for now - float3 vNDC = ScreenPosToNDC(PassSrg::m_linearDepth, input.Position.xy, input.Position.z); - uint2 dimensions; - PassSrg::m_linearDepth.GetDimensions(dimensions.x, dimensions.y); +// float3 vNDC = ScreenPosToNDC(PassSrg::m_linearDepth, input.Position.xy, input.Position.z); +// uint2 dimensions; +// PassSrg::m_linearDepth.GetDimensions(dimensions.x, dimensions.y); // float coverage = ComputeCoverage(input.p0p1.xy, input.p0p1.zw, vNDC.xy, float2(dimensions.x, dimensions.y)); float coverage = 1.0; ///////////////////////////////////////////////////////////////////// diff --git a/Gems/AtomTressFX/Assets/Shaders/HairRenderingResolvePPLL.azsl b/Gems/AtomTressFX/Assets/Shaders/HairRenderingResolvePPLL.azsl index 82c75280f6..3d72f1e21e 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairRenderingResolvePPLL.azsl +++ b/Gems/AtomTressFX/Assets/Shaders/HairRenderingResolvePPLL.azsl @@ -58,7 +58,7 @@ ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback // in the OIT process. // It can also be used to avoid the HW blend done at the end of the pixel // shader stage but HW blend might be cheaper than additional PS blend. - Texture2D m_frameBuffer; // The merged MSAA output + Texture2D m_frameBuffer; // The merged non-MSAA input // Linear depth is used for getting the screen to world transform Texture2D m_linearDepth; @@ -93,26 +93,11 @@ ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback #define HairParams PassSrg::m_hairParams //============================================================================== +#include // provides the Vertex Shader #include -#include -#include - -// Generates a fullscreen triangle from pipeline provided vertex id -VSOutput FullScreenVS(VSInput input) -{ - VSOutput OUT; - - float4 posTex = GetVertexPositionAndTexCoords(input.m_vertexID); - - OUT.m_texCoord = float2(posTex.z, posTex.w); - OUT.m_position = float4(posTex.x, posTex.y, 0.0, 1.0); - - return OUT; -} ////////////////////////////////////////////////////////////// // Bind data for PPLLResolvePS - #define NODE_DATA(x) LinkedListNodes[x].data #define NODE_NEXT(x) LinkedListNodes[x].uNext #define NODE_DEPTH(x) LinkedListNodes[x].depth @@ -298,7 +283,7 @@ float4 GatherLinkedList(float2 vfScreenAddress, float2 screenUV, inout float out uint shadeParamIndex; // So we know what settings to shade with float3 vColor = UnpackUintIntoFloat3Byte(color, shadeParamIndex); - float3 fragmentColor = TressFXShading(vfScreenAddress, fDepth, vTangent, vColor, fcolor.w, shadeParamIndex); + float3 fragmentColor = TressFXShadingFullScreen(vfScreenAddress, fDepth, vTangent, vColor, fcolor.w, shadeParamIndex); // Blend in the fragment color fcolor.xyz = fcolor.xyz * (1.f - alpha) + fragmentColor * alpha; @@ -355,7 +340,7 @@ float4 GetClosestFragment(float2 vfScreenAddress, float2 screenUV, inout float c float alpha = 1.0; uint shadeParamIndex; // the material index float3 vColor = UnpackUintIntoFloat3Byte(curColor, shadeParamIndex); - float3 fragmentColor = TressFXShading(vfScreenAddress, curDepth, vTangent, vColor, fcolor.w, shadeParamIndex); + float3 fragmentColor = TressFXShadingFullScreen(vfScreenAddress, curDepth, vTangent, vColor, fcolor.w, shadeParamIndex); // Blend in the fragment color fcolor.xyz = fcolor.xyz * (1.f - alpha) + (fragmentColor * alpha); diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.azsl b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.azsl new file mode 100644 index 0000000000..23c926406f --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.azsl @@ -0,0 +1,131 @@ +/* +* Modifications 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) AND MIT +* +*/ + +// +// Copyright (c) 2019 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include +#include + +//!------------------------------ SRG Structure -------------------------------- +//! Per pass SRG that holds the dynamic shared read-write buffer shared +//! across all dispatches and draw calls. It is used for all the dynamic buffers +//! that can change between passes due to the application of skinning, simulation +//! and physics affect. +//! Once the compute pases are done, it is read by the rendering shaders. +ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback +{ + //! This shared buffer needs to match the SharedBuffer structure + //! shared between all draw calls / dispatches for the hair skinning + StructuredBuffer m_skinnedHairSharedBuffer; + + //! Based on [[vk::binding(0, 3)]] RWTexture2DArray RWFragmentDepthsTexture : register(u0, space3); + RWTexture2DArray m_RWFragmentDepthsTexture; +} +//============================================================================== + +//!------------------------------ SRG Structure -------------------------------- +//! Per instance/draw SRG representing dynamic read-write set of buffers +//! that are unique per instance and are shared and changed between passes due +//! to the application of skinning, simulation and physics affect. +//! It is then also read by the rendering shaders. +//! This Srg is NOT shared by the passes since it requires having barriers between +//! both passes and draw calls, instead, all buffers are allocated from a single +//! shared buffer (through BufferViews) and that buffer is then shared between +//! the passes via the PerPass Srg frequency. +ShaderResourceGroup HairDynamicDataSrg : SRG_PerObject // space 1 - per instance / object +{ + Buffer m_hairVertexPositions; + Buffer m_hairVertexTangents; + + //! Per hair object offset to the start location of each buffer within + //! 'm_skinnedHairSharedBuffer'. The offset is in bytes! + uint m_positionBufferOffset; + uint m_tangentBufferOffset; +}; +//------------------------------------------------------------------------------ +// Allow for the code to run with minimal changes - skinning / simulation compute passes +// Usage of per-instance buffer +#define g_GuideHairVertexPositions HairDynamicDataSrg::m_hairVertexPositions +#define g_GuideHairVertexTangents HairDynamicDataSrg::m_hairVertexTangents +//------------------------------------------------------------------------------ + +#include // VS resides here + +//!============================================================================= +//! Geometry Depth Alpha - First Pass of ShortCut Render +//! It is a Geometry pass that stores the K=3 front fragment depths, and accumulates +//! product of 1-alpha multiplications (fade out) of the input render target. +//! +//! Short explanation: in the original AMD implementation 1-alpha is multiplied +//! repeatedly with the incoming render target (back buffer) hence blending out +//! the existing back buffer color based on the density and transparency of the hair. +//! This implies that later on the hair color should be added based on the inverse +//! of this operation. +//!============================================================================= +[earlydepthstencil] +float HairShortCutDepthsAlphaPS(PS_INPUT_HAIR input) : SV_Target +{ + ////////////////////////////////////////////////////////////////////// + // [To Do] Hair: anti aliasing via coverage requires work and is disabled for now +// float3 vNDC = ScreenPosToNDC(PassSrg::m_linearDepth, input.Position.xy, input.Position.z); +// uint2 dimensions; +// PassSrg::m_linearDepth.GetDimensions(dimensions.x, dimensions.y); +// float coverage = ComputeCoverage(input.p0p1.xy, input.p0p1.zw, vNDC.xy, float2(dimensions.x, dimensions.y)); + float coverage = 1.0; + ///////////////////////////////////////////////////////////////////// + + float alpha = coverage * MatBaseColor.a; + + if (alpha < SHORTCUT_MIN_ALPHA) + return 1.0; + + int2 vScreenAddress = int2(input.Position.xy); + uint uDepth = asuint(input.Position.z); + uint uDepth0Prev, uDepth1Prev, uDepth2Prev; + + // Min of depth 0 and input depth - in Atom the Z order is reverse + // Original value is uDepth0Prev + InterlockedMax(PassSrg::m_RWFragmentDepthsTexture[uint3(vScreenAddress, 0)], uDepth, uDepth0Prev); + + // Min of depth 1 and greater of the last compare - in Atom the Z order is reverse + // If fragment opaque, always use input depth (don't need greater depths) + uDepth = (alpha > 0.98) ? uDepth : max(uDepth, uDepth0Prev); + + InterlockedMax(PassSrg::m_RWFragmentDepthsTexture[uint3(vScreenAddress, 1)], uDepth, uDepth1Prev); + + // Min of depth 2 and greater of the last compare - in Atom the Z order is reverse + // If fragment opaque, always use input depth (don't need greater depths) + uDepth = (alpha > 0.98) ? uDepth : max(uDepth, uDepth1Prev); + + InterlockedMax(PassSrg::m_RWFragmentDepthsTexture[uint3(vScreenAddress, 2)], uDepth, uDepth2Prev); + + // Accumulate the alpha multiplication from all hair components by multiplying the inverse and + // therefore going down towards 0. At the end product, the inverse will be taken as the hair + // alpha and the remainder will be used to blend the back buffer. + return 1.0 - alpha; +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.shader b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.shader new file mode 100644 index 0000000000..7cd1f44510 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.shader @@ -0,0 +1,45 @@ +{ + "Source" : "HairShortCutGeometryDepthAlpha.azsl", + "DrawList" : "HairGeometryDepthAlphaDrawList", + + "DepthStencilState" : + { + "Depth" : + { + "Enable" : true, + "WriteMask" : "Zero", // Avoid writing the depth + "CompareFunc" : "GreaterEqual" + // Originally in TressFX this is LessEqual - Atom is using reverse sort + }, + "Stencil" : + { + "Enable" : false + } + }, + + "BlendState" : + { + "Enable" : true, + "BlendSource" : "Zero", + "BlendDest" : "ColorSource", + "BlendOp" : "Add", + "BlendAlphaSource" : "Zero", + "BlendAlphaDest" : "AlphaSource", + "BlendAlphaOp" : "Add" + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "RenderHairVS", + "type": "Vertex" + }, + { + "name": "HairShortCutDepthsAlphaPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.azsl b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.azsl new file mode 100644 index 0000000000..c2a2958dfe --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.azsl @@ -0,0 +1,176 @@ +/* +* Modifications 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) AND MIT +* +*/ + +//------------------------------------------------------------------------------ +// Shader code related to lighting and shadowing for TressFX +//------------------------------------------------------------------------------ +// +// Copyright (c) 2019 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include +#include + +#define AMD_TRESSFX_MAX_HAIR_GROUP_RENDER 16 + +//!------------------------------ SRG Structure -------------------------------- +//! Per pass SRG that holds the dynamic shared read-write buffer shared +//! across all dispatches and draw calls. It is used for all the dynamic buffers +//! that can change between passes due to the application of skinning, simulation +//! and physics affect. +//! Once the compute pases are done, it is read by the rendering shaders. +ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback +{ + //! This shared buffer needs to match the SharedBuffer structure + //! shared between all draw calls / dispatches for the hair skinning + StructuredBuffer m_skinnedHairSharedBuffer; + + //! Per hair object material array used by the PPLL resolve pass + //! Originally in TressFXRendering.hlsl this is space 0 + HairObjectShadeParams m_hairParams[AMD_TRESSFX_MAX_HAIR_GROUP_RENDER]; + + // Linear depth is used for getting the screen to world transform + Texture2D m_linearDepth; + + //------------------------------ + // Lighting Data + //------------------------------ + Sampler LinearSampler + { // Required by LightingData.azsli + MinFilter = Linear; + MagFilter = Linear; + MipFilter = Linear; + AddressU = Clamp; + AddressV = Clamp; + AddressW = Clamp; + }; + + Texture2DArray m_directionalLightShadowmap; + Texture2DArray m_directionalLightExponentialShadowmap; + Texture2DArray m_projectedShadowmaps; + Texture2DArray m_projectedExponentialShadowmap; + Texture2D m_brdfMap; + Texture2D m_tileLightData; + StructuredBuffer m_lightListRemapped; +} + +//------------------------------------------------------------------------------ +//! The hair objects' material array buffer used by the rendering resolve pass +#define HairParams PassSrg::m_hairParams +//============================================================================== + +//!------------------------------ SRG Structure -------------------------------- +//! Per instance/draw SRG representing dynamic read-write set of buffers +//! that are unique per instance and are shared and changed between passes due +//! to the application of skinning, simulation and physics affect. +//! It is then also read by the rendering shaders. +//! This Srg is NOT shared by the passes since it requires having barriers between +//! both passes and draw calls, instead, all buffers are allocated from a single +//! shared buffer (through BufferViews) and that buffer is then shared between +//! the passes via the PerPass Srg frequency. +ShaderResourceGroup HairDynamicDataSrg : SRG_PerObject // space 1 - per instance / object +{ + Buffer m_hairVertexPositions; + Buffer m_hairVertexTangents; + + //! Per hair object offset to the start location of each buffer within + //! 'm_skinnedHairSharedBuffer'. The offset is in bytes! + uint m_positionBufferOffset; + uint m_tangentBufferOffset; +}; +//------------------------------------------------------------------------------ +// Allow for the code to run with minimal changes - skinning / simulation compute passes +// Usage of per-instance buffer +#define g_GuideHairVertexPositions HairDynamicDataSrg::m_hairVertexPositions +#define g_GuideHairVertexTangents HairDynamicDataSrg::m_hairVertexTangents +//------------------------------------------------------------------------------ + +#include // VS resides here +#include // Required for world coordinates calculation +#include + +//!============================================================================= +//! Geometry Shading - Third Pass of ShortCut Render +//! Geometry pass that shades pixels that passes the early depth test. Due to this, it +//! is limited to the stored K near fragments due to previous depth write pass that +//! wrote the furthest depth of the K stored depths. +//! Colors are accumulated in the render target for a weighted average in final pass. +//! [To Do] - in the original short cut, the alpha is taken from the depth alpha pass +//!============================================================================= +[earlydepthstencil] +float4 HairShortCutGeometryColorPS(PS_INPUT_HAIR input) : SV_Target +{ + // Strand Color read in is either the BaseMatColor, or BaseMatColor modulated with a color read from texture + // on vertex shader for base color along with modulation by the tip color + float4 strandColor = float4(input.StrandColor.rgb, MatBaseColor.a); + + // If we are supporting strand UV texturing, further blend in the texture color/alpha + // Do this while computing NDC and coverage to hide latency from texture lookup + if (EnableStrandUV) + { + // Grab the uv in case we need it + float2 uv = float2(input.Tangent.w, input.StrandColor.w); + + // Apply StrandUVTiling + float2 strandUV = float2(uv.x, (uv.y * StrandUVTilingFactor) - floor(uv.y * StrandUVTilingFactor)); + + strandColor.rgb *= StrandAlbedoTexture.Sample(LinearWrapSampler, strandUV).rgb; + } + + ////////////////////////////////////////////////////////////////////// + // [To Do] Hair: anti aliasing via coverage requires work and is disabled for now +// float3 vNDC = ScreenPosToNDC(PassSrg::m_linearDepth, input.Position.xy, input.Position.z); +// uint2 dimensions; +// PassSrg::m_linearDepth.GetDimensions(dimensions.x, dimensions.y); +// float2 screenCoords = saturate(pixelCoord / dimensions.xy); +// float coverage = ComputeCoverage(input.p0p1.xy, input.p0p1.zw, vNDC.xy, float2(dimensions.x, dimensions.y)); +// original: float coverage = ComputeCoverage(input.p0p1.xy, input.p0p1.zw, vNDC.xy, g_vViewport.zw - g_vViewport.xy); + float coverage = 1.0; + ///////////////////////////////////////////////////////////////////// + + float alpha = coverage; + + // Update the alpha to have proper value (accounting for coverage, base alpha, and strand alpha) + alpha *= strandColor.w; + + // Early out + if (alpha < SHORTCUT_MIN_ALPHA) + { + return float4(0, 0, 0, 0); + } + + float2 pixelCoord = input.Position.xy; + float depth = input.Position.z; + // [To Do] - the thickness will need to be corrected somehow since this technique doesn't + // keeps track of the accumulated alpha / thickness + float thickness = alpha; + float3 shadedFragment = TressFXShading(pixelCoord, depth, input.Tangent.xyz, strandColor.rgb, thickness, RenderParamsIndex); + + // Color channel: Pre-multiply with alpha to create non-normalized weighted sum. + // Alpha Channel: Sum up all the hair alphas - this will be used to normalize the color + // per fragment at the next pass. + return float4(shadedFragment * alpha, alpha); +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.shader b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.shader new file mode 100644 index 0000000000..40e56006cd --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.shader @@ -0,0 +1,45 @@ +{ + "Source" : "HairShortCutGeometryShading.azsl", + "DrawList" : "HairGeometryShadingDrawList", + + "DepthStencilState" : + { + "Depth" : + { + "Enable" : true, + "WriteMask" : "Zero", // Avoid writing the depth + "CompareFunc" : "GreaterEqual" + // Originally in TressFX this is LessEqual - Atom is using reverse sort + }, + "Stencil" : + { + "Enable" : false + } + }, + + "BlendState" : + { + "Enable" : true, + "BlendSource" : "One", + "BlendDest" : "One", + "BlendOp" : "Add", + "BlendAlphaSource" : "One", + "BlendAlphaDest" : "One", + "BlendAlphaOp" : "Add" + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "RenderHairVS", + "type": "Vertex" + }, + { + "name": "HairShortCutGeometryColorPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.azsl b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.azsl new file mode 100644 index 0000000000..f25c9dd711 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.azsl @@ -0,0 +1,63 @@ +/* +* Modifications 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) AND MIT +* +*/ + +#include +#include + +//!------------------------------ SRG Structure -------------------------------- +//! Per pass SRG that holds the dynamic shared read-write buffer shared +//! across all dispatches and draw calls. It is used for all the dynamic buffers +//! that can change between passes due to the application of skinning, simulation +//! and physics affect. +//! Once the compute pases are done, it is read by the rendering shaders. +ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback +{ + // oiriginally: [[vk::binding(0, 0)]] Texture2D HaiColorTexture : register(t0, space0); + // oiriginally: [[vk::binding(1, 0)]] Texture2D AccumInvAlpha : register(t1, space0); + Texture2D m_hairColorTexture; + Texture2D m_accumInvAlpha; +} +//------------------------------------------------------------------------------ + +#include // provides the Vertex Shader + +//!============================================================================= +//! HairColorPS - Fourth Pass of ShortCut Render +//! Full-screen pass that finalizes the weighted average, and blends using the +//! accumulated 1-alpha product. +//!============================================================================= +[earlydepthstencil] +float4 HairShortCutResolveColorPS(VSOutput input) : SV_Target +{ + int2 vScreenAddress = int2(input.m_position.xy); + + float fInvAlpha = PassSrg::m_accumInvAlpha[vScreenAddress]; + float fAlpha = 1.0 - fInvAlpha; + + if (fAlpha < SHORTCUT_MIN_ALPHA) + { + // next we discard of non-hair pixels to avoid manipulating them depending + // on the alpha blend state - this is the safer and faster approach as there + // is no hair in these pixels + discard; + } + + float4 finalColor; + float weightSum = PassSrg::m_hairColorTexture[vScreenAddress].w; + + // Normalize the sum of the shaded fragment from the previous pass and + // then multiply it by the alpha blend of the hairs done in the depth-alpha pass. + finalColor.xyz = PassSrg::m_hairColorTexture[vScreenAddress] * fAlpha / weightSum; + + // The alpha is set to the inverse alpha of the hair so that the original + // background will be blended using this factor emulating single step alpha blend + // over the sum of all hair fragment blends. + finalColor.w = fInvAlpha; + + return finalColor; +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.shader b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.shader new file mode 100644 index 0000000000..6703c63f18 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.shader @@ -0,0 +1,41 @@ +{ + "Source" : "HairShortCutResolveColor.azsl", + + "DepthStencilState" : + { + "Depth" : + { + "Enable" : false // Avoid comparing depth + }, + "Stencil" : + { + "Enable" : false + } + }, + + "BlendState" : + { + "Enable" : true, + "BlendSource" : "One", + "BlendDest" : "AlphaSource", + "BlendOp" : "Add", + "BlendAlphaSource" : "Zero", + "BlendAlphaDest" : "Zero", + "BlendAlphaOp" : "Add" + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "FullScreenVS", + "type": "Vertex" + }, + { + "name": "HairShortCutResolveColorPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.azsl b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.azsl new file mode 100644 index 0000000000..862b7c9015 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.azsl @@ -0,0 +1,65 @@ +/* +* Modifications 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) AND MIT +* +*/ + +// +// Copyright (c) 2019 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include + +//!------------------------------ SRG Structure -------------------------------- +//! Per pass SRG that holds the dynamic shared read-write buffer shared +//! across all dispatches and draw calls. It is used for all the dynamic buffers +//! that can change between passes due to the application of skinning, simulation +//! and physics affect. +//! Once the compute pases are done, it is read by the rendering shaders. +ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback +{ + // Originally: [[vk::binding(0, 0)]] Texture2DArray FragmentDepthsTexture : register(t0, space0); + Texture2DArray m_fragmentDepthsTexture; +} +//------------------------------------------------------------------------------ + +#include // provides the Vertex Shader + +//!============================================================================= +//! Resolve Depth - Second Pass of ShortCut +//! Full-screen pass that writes the farthest of the stored K near depths so it +//! could be used for depth culling during the following geometry shading pass. +//!============================================================================= +float HairShortCutResolveDepthPS(VSOutput input) : SV_Depth +{ + // Blend the layers of fragments from back to front + int2 vScreenAddress = int2(input.m_position.xy); + + // Write farthest depth value for culling in the next pass. + // It may be the initial value of 1.0 if there were not enough fragments to write all depths, but then culling not important. + const int farthestDepthIndex = 2; + uint uDepth = PassSrg::m_fragmentDepthsTexture[uint3(vScreenAddress, farthestDepthIndex)]; + + // The following line is writing the depth into the actual depth buffer + return asfloat(uDepth); +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.shader b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.shader new file mode 100644 index 0000000000..5b518c3bfc --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.shader @@ -0,0 +1,37 @@ +{ + "Source" : "HairShortCutResolveDepth.azsl", + + "DepthStencilState" : + { + "Depth" : + { + "Enable" : true, // test the written depth and accept/discard based on the depth buffer + "CompareFunc" : "GreaterEqual" + // Originally in TressFX this is LessEqual - Atom is using reverse sort + }, + "Stencil" : + { + "Enable" : false + } + }, + + "BlendState" : + { + "Enable" : false + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "FullScreenVS", + "type": "Vertex" + }, + { + "name": "HairShortCutResolveDepthPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairSimulationCompute.azsl b/Gems/AtomTressFX/Assets/Shaders/HairSimulationCompute.azsl index 496aa5c7da..ed6ec5de27 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairSimulationCompute.azsl +++ b/Gems/AtomTressFX/Assets/Shaders/HairSimulationCompute.azsl @@ -31,7 +31,7 @@ // THE SOFTWARE. // //-------------------------------------------------------------------------------------- -#include +#include #include //-------------------------------------------------------------------------------------- diff --git a/Gems/AtomTressFX/Assets/Shaders/HairSimulationSrgs.azsli b/Gems/AtomTressFX/Assets/Shaders/HairSimulationComputeSrgs.azsli similarity index 99% rename from Gems/AtomTressFX/Assets/Shaders/HairSimulationSrgs.azsli rename to Gems/AtomTressFX/Assets/Shaders/HairSimulationComputeSrgs.azsli index f95dac92cf..3ded3ab2af 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairSimulationSrgs.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairSimulationComputeSrgs.azsli @@ -29,13 +29,13 @@ // THE SOFTWARE. // //------------------------------------------------------------------------------ -// File: HairSRGs.azsli +// File: HairSimulationComputeSrgs.azsli // // Declarations of SRGs used by the hair shaders. //------------------------------------------------------------------------------ #pragma once -#include +#include //!----------------------------------------------------------------------------- //! diff --git a/Gems/AtomTressFX/Assets/Shaders/HairStrands.azsli b/Gems/AtomTressFX/Assets/Shaders/HairStrands.azsli index b8c4ea9eb4..abbbc8c0de 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairStrands.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairStrands.azsli @@ -66,12 +66,22 @@ float3 GetSharedTangent(int tangentIndex) ); } +//! Hair vertex geometry output - input structure for the Pixel shaders struct TressFXVertex { float4 Position; - float4 Tangent; + float4 Tangent; // xyz = Tangent, w = Strand U float4 p0p1; - float4 StrandColor; + float4 StrandColor; // xyz = Strand Color, w = Strand V +}; + +//! Matching structure to carry out as VS output / PS input +struct PS_INPUT_HAIR +{ + float4 Position : SV_POSITION; + float4 Tangent : Tangent; + float4 p0p1 : TEXCOORD0; + float4 StrandColor : TEXCOORD1; }; float3 GetStrandColor(int index, float fractionOfStrand) @@ -178,5 +188,26 @@ TressFXVertex GetExpandedTressFXShadowVert(uint vertexId, float3 eye, float2 win return Output; } -// EndHLSL +//!============================================================================= +//! Hair Render VS - Used by all geometry hair shaders +//!============================================================================= +PS_INPUT_HAIR RenderHairVS(uint vertexId : SV_VertexID) +{ + PS_INPUT_HAIR vsOutput; + + // uint2 scrSize; + // PassSrg::m_linearDepth.GetDimensions(scrSize.x, scrSize.y); + // TressFXVertex tressfxVert = GetExpandedTressFXVert(vertexId, g_vEye.xyz, float2(scrSize), g_mVP); + + // [To Do] Hair: the above code should replace the existing but requires modifications to + // the function GetExpandedTressFXVert. + // Note that in Atom g_vViewport is aspect ratio and NOT size. + TressFXVertex tressfxVert = GetExpandedTressFXVert(vertexId, g_vEye.xyz, g_vViewport.zw, g_mVP); + vsOutput.Position = tressfxVert.Position; + vsOutput.Tangent = tressfxVert.Tangent; + vsOutput.p0p1 = tressfxVert.p0p1; + vsOutput.StrandColor = tressfxVert.StrandColor; + + return vsOutput; +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairUtilities.azsli b/Gems/AtomTressFX/Assets/Shaders/HairUtilities.azsli index 1d50d3c040..f91ba1ff3b 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairUtilities.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairUtilities.azsli @@ -34,9 +34,6 @@ #pragma once -#include - - #define SHORTCUT_MIN_ALPHA 0.02 #define TRESSFX_FLOAT_EPSILON 1e-7 @@ -52,40 +49,6 @@ float4 MatrixMult(float4x4 m, float4 v) return mul(m, v); } -// Given the depth buffer depth of the current pixel and the fragment XY position, -// reconstruct the NDC. -// screenCoords - from 0.. dimension of the screen of the current pixel -// screenTexture - screen buffer texture representing the same resolution we work in -// sDepth - the depth buffer depth at the fragment location -// NDC - Normalized Device Coordinates = warped screen space ( -1.1, -1..1, 0..1 ) -float3 ScreenPosToNDC( Texture2D screenTexture, float2 screenCoords, float depth ) -{ - uint2 dimensions; - screenTexture.GetDimensions(dimensions.x, dimensions.y); - float2 UV = saturate(screenCoords / dimensions.xy); - - float x = UV.x * 2.0f - 1.0f; - float y = (1.0f - UV.y) * 2.0f - 1.0f; - float3 NDC = float3(x, y, depth); - - return NDC; -} - -// Given the depth buffer depth of the current pixel and the fragment XY position, -// reconstruct the world space position -float3 ScreenPosToWorldPos( - Texture2D screenTexture, float2 screenCoords, float depth, - inout float3 screenPosNDC ) -{ - screenPosNDC = ScreenPosToNDC(PassSrg::m_linearDepth, screenCoords, depth); - float4 projectedPos = float4(screenPosNDC, 1.0f); // warped projected space [0..1] - float4 positionVS = mul(ViewSrg::m_projectionMatrixInverse, projectedPos); - positionVS /= positionVS.w; // notice the normalization factor - crucial! - float4 positionWS = mul(ViewSrg::m_viewMatrixInverse, positionVS); - - return positionWS.xyz; -} - // Pack a float4 into an uint uint PackFloat4IntoUint(float4 vValue) { diff --git a/Gems/AtomTressFX/Code/Components/HairSystemComponent.cpp b/Gems/AtomTressFX/Code/Components/HairSystemComponent.cpp index 66ac3d2e09..2ae8b9d97d 100644 --- a/Gems/AtomTressFX/Code/Components/HairSystemComponent.cpp +++ b/Gems/AtomTressFX/Code/Components/HairSystemComponent.cpp @@ -80,8 +80,14 @@ namespace AZ // Load the AtomTressFX pass classes passSystem->AddPassCreator(Name("HairSkinningComputePass"), &HairSkinningComputePass::Create); + + // Load the PPLL render method passes passSystem->AddPassCreator(Name("HairPPLLRasterPass"), &HairPPLLRasterPass::Create); passSystem->AddPassCreator(Name("HairPPLLResolvePass"), &HairPPLLResolvePass::Create); + + // Load the ShortCut render method passes + passSystem->AddPassCreator(Name("HairShortCutGeometryDepthAlphaPass"), &HairShortCutGeometryDepthAlphaPass::Create); + passSystem->AddPassCreator(Name("HairShortCutGeometryShadingPass"), &HairShortCutGeometryShadingPass::Create); } void HairSystemComponent::Deactivate() diff --git a/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.cpp b/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.cpp index 7af5903cf3..b9c0d2e379 100644 --- a/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.cpp +++ b/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.cpp @@ -165,6 +165,15 @@ namespace AZ return true; } + Data::Instance HairGeometryRasterPass::GetShader() + { + if (!m_initialized || !m_shader) + { + AZ_Error("Hair Gem", LoadShaderAndPipelineState(), "HairGeometryRasterPass could not initialize pipeline or shader"); + } + return m_shader; + } + void HairGeometryRasterPass::SchedulePacketBuild(HairRenderObject* hairObject) { m_newRenderObjects.insert(hairObject); @@ -188,7 +197,7 @@ namespace AZ // The PerPass is gathered through the RasterPass::m_shaderResourceGroup AZStd::lock_guard lock(m_mutex); - return hairObject->BuildPPLLDrawPacket(drawRequest); + return hairObject->BuildDrawPacket(m_shader.get(), drawRequest); } bool HairGeometryRasterPass::AddDrawPackets(AZStd::list>& hairRenderObjects) @@ -205,7 +214,7 @@ namespace AZ for (auto& renderObject : hairRenderObjects) { - const RHI::DrawPacket* drawPacket = renderObject->GetFillDrawPacket(); + const RHI::DrawPacket* drawPacket = renderObject->GetGeometrylDrawPacket(m_shader.get()); if (!drawPacket) { // might not be an error - the object might have just been added and the DrawPacket is // scheduled to be built when the render frame begins diff --git a/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.h b/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.h index a226d2c294..d3ba22039d 100644 --- a/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.h +++ b/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.h @@ -51,7 +51,7 @@ namespace AZ //! The following will be called when an object was added or shader has been compiled void SchedulePacketBuild(HairRenderObject* hairObject); - Data::Instance GetShader() { return m_shader; } + Data::Instance GetShader(); void SetFeatureProcessor(HairFeatureProcessor* featureProcessor) { @@ -76,7 +76,6 @@ namespace AZ // Pass behavior overrides void InitializeInternal() override; -// void BuildInternal() override; void FrameBeginInternal(FramePrepareParams params) override; // Scope producer functions... diff --git a/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.cpp b/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.cpp index fa643a5488..de66d91e0b 100644 --- a/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.cpp +++ b/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.cpp @@ -30,6 +30,17 @@ namespace AZ HairPPLLResolvePass::HairPPLLResolvePass(const RPI::PassDescriptor& descriptor) : RPI::FullscreenTrianglePass(descriptor) { + o_enableShadows = AZ::Name("o_enableShadows"); + o_enableDirectionalLights = AZ::Name("o_enableDirectionalLights"); + o_enablePunctualLights = AZ::Name("o_enablePunctualLights"); + o_enableAreaLights = AZ::Name("o_enableAreaLights"); + o_enableIBL = AZ::Name("o_enableIBL"); + o_hairLightingModel = AZ::Name("o_hairLightingModel"); + o_enableMarschner_R = AZ::Name("o_enableMarschner_R"); + o_enableMarschner_TRT = AZ::Name("o_enableMarschner_TRT"); + o_enableMarschner_TT = AZ::Name("o_enableMarschner_TT"); + o_enableLongtitudeCoeff = AZ::Name("o_enableLongtitudeCoeff"); + o_enableAzimuthCoeff = AZ::Name("o_enableAzimuthCoeff"); } void HairPPLLResolvePass::UpdateGlobalShaderOptions() @@ -38,17 +49,17 @@ namespace AZ m_featureProcessor->GetHairGlobalSettings(m_hairGlobalSettings); - shaderOption.SetValue(AZ::Name("o_enableShadows"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableShadows }); - shaderOption.SetValue(AZ::Name("o_enableDirectionalLights"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableDirectionalLights }); - shaderOption.SetValue(AZ::Name("o_enablePunctualLights"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enablePunctualLights }); - shaderOption.SetValue(AZ::Name("o_enableAreaLights"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAreaLights }); - shaderOption.SetValue(AZ::Name("o_enableIBL"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableIBL }); - shaderOption.SetValue(AZ::Name("o_hairLightingModel"), AZ::Name{ "HairLightingModel::" + AZStd::string(HairLightingModelNamespace::ToString(m_hairGlobalSettings.m_hairLightingModel)) }); - shaderOption.SetValue(AZ::Name("o_enableMarschner_R"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_R }); - shaderOption.SetValue(AZ::Name("o_enableMarschner_TRT"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TRT }); - shaderOption.SetValue(AZ::Name("o_enableMarschner_TT"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TT }); - shaderOption.SetValue(AZ::Name("o_enableLongtitudeCoeff"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableLongtitudeCoeff }); - shaderOption.SetValue(AZ::Name("o_enableAzimuthCoeff"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAzimuthCoeff }); + shaderOption.SetValue(o_enableShadows, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableShadows }); + shaderOption.SetValue(o_enableDirectionalLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableDirectionalLights }); + shaderOption.SetValue(o_enablePunctualLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enablePunctualLights }); + shaderOption.SetValue(o_enableAreaLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAreaLights }); + shaderOption.SetValue(o_enableIBL, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableIBL }); + shaderOption.SetValue(o_hairLightingModel, AZ::Name{ "HairLightingModel::" + AZStd::string(HairLightingModelNamespace::ToString(m_hairGlobalSettings.m_hairLightingModel)) }); + shaderOption.SetValue(o_enableMarschner_R, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_R }); + shaderOption.SetValue(o_enableMarschner_TRT, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TRT }); + shaderOption.SetValue(o_enableMarschner_TT, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TT }); + shaderOption.SetValue(o_enableLongtitudeCoeff, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableLongtitudeCoeff }); + shaderOption.SetValue(o_enableAzimuthCoeff, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAzimuthCoeff }); m_shaderOptions = shaderOption.GetShaderVariantKeyFallbackValue(); } diff --git a/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.h b/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.h index bf1a3f768e..036659b9a5 100644 --- a/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.h +++ b/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.h @@ -58,9 +58,20 @@ namespace AZ void CompileResources(const RHI::FrameGraphCompileContext& context) override; private: + AZ::Name o_enableShadows; + AZ::Name o_enableDirectionalLights; + AZ::Name o_enablePunctualLights; + AZ::Name o_enableAreaLights; + AZ::Name o_enableIBL; + AZ::Name o_hairLightingModel; + AZ::Name o_enableMarschner_R; + AZ::Name o_enableMarschner_TRT; + AZ::Name o_enableMarschner_TT; + AZ::Name o_enableLongtitudeCoeff; + AZ::Name o_enableAzimuthCoeff; + HairPPLLResolvePass(const RPI::PassDescriptor& descriptor); - private: void UpdateGlobalShaderOptions(); HairGlobalSettings m_hairGlobalSettings; diff --git a/Gems/AtomTressFX/Code/Passes/HairParentPass.cpp b/Gems/AtomTressFX/Code/Passes/HairParentPass.cpp index 24cd6465dd..1b0b034dba 100644 --- a/Gems/AtomTressFX/Code/Passes/HairParentPass.cpp +++ b/Gems/AtomTressFX/Code/Passes/HairParentPass.cpp @@ -9,7 +9,6 @@ #include #include #include -#include namespace AZ { diff --git a/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.cpp b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.cpp new file mode 100644 index 0000000000..5e402e33f6 --- /dev/null +++ b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.cpp @@ -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 + * + */ + +#include +#include +#include + +#include +#include +#include + +namespace AZ +{ + namespace Render + { + namespace Hair + { + + HairShortCutGeometryDepthAlphaPass::HairShortCutGeometryDepthAlphaPass(const RPI::PassDescriptor& descriptor) + : HairGeometryRasterPass(descriptor) + { + SetShaderPath("Shaders/hairshortcutgeometrydepthalpha.azshader"); + } + + RPI::Ptr HairShortCutGeometryDepthAlphaPass::Create(const RPI::PassDescriptor& descriptor) + { + RPI::Ptr pass = aznew HairShortCutGeometryDepthAlphaPass(descriptor); + return pass; + } + + void HairShortCutGeometryDepthAlphaPass::BuildInternal() + { + RasterPass::BuildInternal(); // change this to call parent if the method exists + + if (!AcquireFeatureProcessor()) + { + return; + } + + LoadShaderAndPipelineState(); + } + + } // namespace Hair + } // namespace Render +} // namespace AZ diff --git a/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.h b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.h new file mode 100644 index 0000000000..da6e28fe31 --- /dev/null +++ b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.h @@ -0,0 +1,49 @@ +/* + * 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 + +namespace AZ +{ + namespace RHI + { + struct DrawItem; + } + + namespace Render + { + namespace Hair + { + //! This geometry pass uses the following Srgs: + //! - PerPassSrg shared by all hair passes for the shared dynamic buffer + //! - PerMaterialSrg - used solely by this pass to alter the vertices and apply the visual + //! hair properties to each fragment. + //! - HairDynamicDataSrg (PerObjectSrg) - shared buffers views for this hair object only. + //! - PerViewSrg and PerSceneSrg - as per the data from Atom. + class HairShortCutGeometryDepthAlphaPass + : public HairGeometryRasterPass + { + AZ_RPI_PASS(HairShortCutGeometryDepthAlphaPass); + + public: + AZ_RTTI(HairShortCutGeometryDepthAlphaPass, "{F09A0411-B1FF-4085-98E7-6B8B0E1B2C3D}", HairGeometryRasterPass); + AZ_CLASS_ALLOCATOR(HairShortCutGeometryDepthAlphaPass, SystemAllocator, 0); + + static RPI::Ptr Create(const RPI::PassDescriptor& descriptor); + + protected: + explicit HairShortCutGeometryDepthAlphaPass(const RPI::PassDescriptor& descriptor); + + // Pass behavior overrides + void BuildInternal() override; + }; + + } // namespace Hair + } // namespace Render +} // namespace AZ diff --git a/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.cpp b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.cpp new file mode 100644 index 0000000000..eda7f9f83a --- /dev/null +++ b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.cpp @@ -0,0 +1,111 @@ +/* + * 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 AZ +{ + namespace Render + { + namespace Hair + { + + HairShortCutGeometryShadingPass::HairShortCutGeometryShadingPass(const RPI::PassDescriptor& descriptor) + : HairGeometryRasterPass(descriptor) + { + o_enableShadows = AZ::Name("o_enableShadows"); + o_enableDirectionalLights = AZ::Name("o_enableDirectionalLights"); + o_enablePunctualLights = AZ::Name("o_enablePunctualLights"); + o_enableAreaLights = AZ::Name("o_enableAreaLights"); + o_enableIBL = AZ::Name("o_enableIBL"); + o_hairLightingModel = AZ::Name("o_hairLightingModel"); + o_enableMarschner_R = AZ::Name("o_enableMarschner_R"); + o_enableMarschner_TRT = AZ::Name("o_enableMarschner_TRT"); + o_enableMarschner_TT = AZ::Name("o_enableMarschner_TT"); + o_enableLongtitudeCoeff = AZ::Name("o_enableLongtitudeCoeff"); + o_enableAzimuthCoeff = AZ::Name("o_enableAzimuthCoeff"); + + SetShaderPath("Shaders/hairshortcutgeometryshading.azshader"); + } + + RPI::Ptr HairShortCutGeometryShadingPass::Create(const RPI::PassDescriptor& descriptor) + { + RPI::Ptr pass = aznew HairShortCutGeometryShadingPass(descriptor); + return pass; + } + + void HairShortCutGeometryShadingPass::UpdateGlobalShaderOptions() + { + RPI::ShaderOptionGroup shaderOption = m_shader->CreateShaderOptionGroup(); + + m_featureProcessor->GetHairGlobalSettings(m_hairGlobalSettings); + + shaderOption.SetValue(o_enableShadows, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableShadows }); + shaderOption.SetValue(o_enableDirectionalLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableDirectionalLights }); + shaderOption.SetValue(o_enablePunctualLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enablePunctualLights }); + shaderOption.SetValue(o_enableAreaLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAreaLights }); + shaderOption.SetValue(o_enableIBL, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableIBL }); + shaderOption.SetValue(o_hairLightingModel, AZ::Name{ "HairLightingModel::" + AZStd::string(HairLightingModelNamespace::ToString(m_hairGlobalSettings.m_hairLightingModel)) }); + shaderOption.SetValue(o_enableMarschner_R, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_R }); + shaderOption.SetValue(o_enableMarschner_TRT, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TRT }); + shaderOption.SetValue(o_enableMarschner_TT, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TT }); + shaderOption.SetValue(o_enableLongtitudeCoeff, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableLongtitudeCoeff }); + shaderOption.SetValue(o_enableAzimuthCoeff, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAzimuthCoeff }); + + m_shaderOptions = shaderOption.GetShaderVariantKeyFallbackValue(); + } + + void HairShortCutGeometryShadingPass::CompileResources(const RHI::FrameGraphCompileContext& context) + { + if (!m_shaderResourceGroup || !AcquireFeatureProcessor()) + { + AZ_Error("Hair Gem", m_shaderResourceGroup, "HairShortCutGeometryShadingPass: missing Srg or no feature processor yet"); + return; // no error message due to FP - initialization not complete yet, wait for the next frame + } + + UpdateGlobalShaderOptions(); + + if (m_shaderResourceGroup->HasShaderVariantKeyFallbackEntry()) + { + m_shaderResourceGroup->SetShaderVariantKeyFallbackValue(m_shaderOptions); + } + + // Update the material array constant buffer within the per pass srg + SrgBufferDescriptor descriptor = SrgBufferDescriptor( + RPI::CommonBufferPoolType::Constant, RHI::Format::Unknown, + sizeof(AMD::TressFXShadeParams), 1, + Name{ "HairMaterialsArray" }, Name{ "m_hairParams" }, 0, 0 + ); + + m_featureProcessor->GetMaterialsArray().UpdateGPUData(m_shaderResourceGroup, descriptor); + + // Compilation of remaining srgs will be done by the parent class + RPI::RasterPass::CompileResources(context); + } + + void HairShortCutGeometryShadingPass::BuildInternal() + { + RasterPass::BuildInternal(); // change this to call parent if the method exists + + if (!AcquireFeatureProcessor()) + { + return; + } + + LoadShaderAndPipelineState(); + } + + } // namespace Hair + } // namespace Render +} // namespace AZ diff --git a/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.h b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.h new file mode 100644 index 0000000000..d728803473 --- /dev/null +++ b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.h @@ -0,0 +1,69 @@ +/* + * 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 + +namespace AZ +{ + namespace RHI + { + struct DrawItem; + } + + namespace Render + { + namespace Hair + { + //! This geometry pass uses the following Srgs: + //! - PerPassSrg shared by all hair passes for the shared dynamic buffer + //! - PerMaterialSrg - used solely by this pass to alter the vertices and apply the visual + //! hair properties to each fragment. + //! - HairDynamicDataSrg (PerObjectSrg) - shared buffers views for this hair object only. + //! - PerViewSrg and PerSceneSrg - as per the data from Atom. + class HairShortCutGeometryShadingPass + : public HairGeometryRasterPass + { + AZ_RPI_PASS(HairShortCutGeometryShadingPass); + + public: + AZ_RTTI(HairShortCutGeometryShadingPass, "{11BA673D-0788-4B25-978D-9737BF4E48FE}", HairGeometryRasterPass); + AZ_CLASS_ALLOCATOR(HairShortCutGeometryShadingPass, SystemAllocator, 0); + + static RPI::Ptr Create(const RPI::PassDescriptor& descriptor); + + void CompileResources(const RHI::FrameGraphCompileContext& context) override; + + protected: + AZ::Name o_enableShadows; + AZ::Name o_enableDirectionalLights; + AZ::Name o_enablePunctualLights; + AZ::Name o_enableAreaLights; + AZ::Name o_enableIBL; + AZ::Name o_hairLightingModel; + AZ::Name o_enableMarschner_R; + AZ::Name o_enableMarschner_TRT; + AZ::Name o_enableMarschner_TT; + AZ::Name o_enableLongtitudeCoeff; + AZ::Name o_enableAzimuthCoeff; + + explicit HairShortCutGeometryShadingPass(const RPI::PassDescriptor& descriptor); + + void UpdateGlobalShaderOptions(); + + // Pass behavior overrides + void BuildInternal() override; + + HairGlobalSettings m_hairGlobalSettings; + AZ::RPI::ShaderVariantKey m_shaderOptions; + }; + + } // namespace Hair + } // namespace Render +} // namespace AZ diff --git a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp index 7a317dc09c..a0be18f0e2 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp +++ b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp @@ -47,11 +47,11 @@ namespace AZ HairFeatureProcessor::HairFeatureProcessor() { - HairParentPassName = Name{ "HairParentPass" }; + m_usePPLLRenderTechnique = false; // Use the ShortCut rendering technique - HairPPLLRasterPassName = Name{ "HairPPLLRasterPass" }; - HairPPLLResolvePassName = Name{ "HairPPLLResolvePass" }; + HairParentPassName = Name{ "HairParentPass" }; + // Hair Skinning and Simulation Compute passes GlobalShapeConstraintsPassName = Name{ "HairGlobalShapeConstraintsComputePass" }; CalculateStrandDataPassName = Name{ "HairCalculateStrandLevelDataComputePass" }; VelocityShockPropagationPassName = Name{ "HairVelocityShockPropagationComputePass" }; @@ -59,12 +59,21 @@ namespace AZ LengthConstriantsWindAndCollisionPassName = Name{ "HairLengthConstraintsWindAndCollisionComputePass" }; UpdateFollowHairPassName = Name{ "HairUpdateFollowHairComputePass" }; + // PPLL render technique pases + HairPPLLRasterPassName = Name{ "HairPPLLRasterPass" }; + HairPPLLResolvePassName = Name{ "HairPPLLResolvePass" }; + + // ShortCut render technique pases + HairShortCutGeometryDepthAlphaPassName = Name{ "HairShortCutGeometryDepthAlphaPass" }; + HairShortCutResolveDepthPassName = Name{ "HairShortCutResolveDepthPass" }; + HairShortCutGeometryShadingPassName = Name{ "HairShortCutGeometryShadingPass" }; + HairShortCutResolveColorPassName = Name{ "HairShortCutResolveColorPass" }; + ++s_instanceCount; if (!CreatePerPassResources()) { // this might not be an error - if the pass system is still empty / minimal - // and these passes are not part of the minimal pipeline, they will not - // be created. + // and these passes are not part of the minimal pipeline, they will not be created. AZ_Error("Hair Gem", false, "Failed to create the hair shared buffer resource"); } } @@ -127,25 +136,19 @@ namespace AZ m_hairRenderObjects.push_back(renderObject); + // Adding the object will schedule Srgs binding and the DrawItem build for the geometry passes. BuildDispatchAndDrawItems(renderObject); EnablePasses(true); } - void HairFeatureProcessor::EnablePasses(bool enable) + void HairFeatureProcessor::EnablePasses([[maybe_unused]] bool enable) { - if (!m_initialized) - { - return; - } - - for (auto& [passName, pass] : m_computePasses) + RPI::Ptr desiredPass = m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairParentPassName); + if (desiredPass) { - pass->SetEnabled(enable); + desiredPass->SetEnabled(enable); } - - m_hairPPLLRasterPass->SetEnabled(enable); - m_hairPPLLResolvePass->SetEnabled(enable); } bool HairFeatureProcessor::RemoveHairRenderObject(Data::Instance renderObject) @@ -167,15 +170,13 @@ namespace AZ void HairFeatureProcessor::UpdateHairSkinning() { - // Copying CPU side m_SimCB content to the GPU buffer (matrices, wind parameters..) - - for (auto objIter = m_hairRenderObjects.begin(); objIter != m_hairRenderObjects.end(); ++objIter) + // Copying CPU side m_SimCB content to the GPU buffer (matrices, wind parameters..) + for (auto& hairRenderObject : m_hairRenderObjects) { - if (!objIter->get()->IsEnabled()) + if (hairRenderObject->IsEnabled()) { - return; + hairRenderObject->Update(); } - objIter->get()->Update(); } } @@ -214,7 +215,8 @@ namespace AZ } if (m_forceRebuildRenderData) - { + { // In the case of a force build, schedule Srgs binding and the DrawItem build for + // the geometry passes of all existing hair objects. for (auto& hairRenderObject : m_hairRenderObjects) { BuildDispatchAndDrawItems(hairRenderObject); @@ -276,17 +278,32 @@ namespace AZ pass->AddDispatchItems(m_hairRenderObjects); } - // Add all hair objects to the Render / Raster Pass - m_hairPPLLRasterPass->AddDrawPackets(m_hairRenderObjects); + if (m_usePPLLRenderTechnique) + { + // Add all hair objects to the Render / Raster Pass + m_hairPPLLRasterPass->AddDrawPackets(m_hairRenderObjects); + } + else + { + m_hairShortCutGeometryDepthAlphaPass->AddDrawPackets(m_hairRenderObjects); + m_hairShortCutGeometryShadingPass->AddDrawPackets(m_hairRenderObjects); + } } void HairFeatureProcessor::ClearPasses() { m_initialized = false; // Avoid simulation or render m_computePasses.clear(); + + // PPLL geometry and resolve full screen passes m_hairPPLLRasterPass = nullptr; m_hairPPLLResolvePass = nullptr; + // ShortCut passes - Special handling of geometry passes only, and using the regular + // full screen pass for the resolve + m_hairShortCutGeometryDepthAlphaPass = nullptr; + m_hairShortCutGeometryShadingPass = nullptr; + // Mark for all passes to evacuate their render data and recreate it. m_forceRebuildRenderData = true; m_forceClearRenderData = true; @@ -338,6 +355,12 @@ namespace AZ ClearPasses(); + if (!m_renderPipeline) + { + AZ_Error("Hair Gem", false, "HairFeatureProcessor does NOT have render pipeline set yet"); + return false; + } + // Compute Passes - populate the passes map bool resultSuccess = InitComputePass(GlobalShapeConstraintsPassName); resultSuccess &= InitComputePass(CalculateStrandDataPassName); @@ -347,8 +370,15 @@ namespace AZ resultSuccess &= InitComputePass(UpdateFollowHairPassName); // Rendering Passes - resultSuccess &= InitPPLLFillPass(); - resultSuccess &= InitPPLLResolvePass(); + if (m_usePPLLRenderTechnique) + { + resultSuccess &= InitPPLLFillPass(); + resultSuccess &= InitPPLLResolvePass(); + } + else + { + resultSuccess &= InitShortCutRenderPasses(); + } m_initialized = resultSuccess; @@ -388,7 +418,8 @@ namespace AZ } } - // PPLL nodes buffer + // PPLL nodes buffer - created only if the PPLL technique is used + if (m_usePPLLRenderTechnique) { descriptor = SrgBufferDescriptor( RPI::CommonBufferPoolType::ReadWrite, RHI::Format::Unknown, @@ -425,11 +456,6 @@ namespace AZ bool HairFeatureProcessor::InitComputePass(const Name& passName, bool allowIterations) { m_computePasses[passName] = nullptr; - if (!m_renderPipeline) - { - AZ_Error("Hair Gem", false, "%s does NOT have render pipeline set yet", passName.GetCStr()); - return false; - } RPI::Ptr desiredPass = m_renderPipeline->GetRootPass()->FindPassByNameRecursive(passName); if (desiredPass) @@ -452,11 +478,6 @@ namespace AZ bool HairFeatureProcessor::InitPPLLFillPass() { m_hairPPLLRasterPass = nullptr; // reset it to null, just in case it fails to load the assets properly - if (!m_renderPipeline) - { - AZ_Error("Hair Gem", false, "Hair Fill Pass does NOT have render pipeline set yet"); - return false; - } RPI::Ptr desiredPass = m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairPPLLRasterPassName); if (desiredPass) @@ -466,7 +487,7 @@ namespace AZ } else { - AZ_Error("Hair Gem", false, "HairPPLLRasterPass does not have any valid passes. Check your game project's .pass assets."); + AZ_Error("Hair Gem", false, "HairPPLLRasterPass cannot be found. Check your game project's .pass assets."); return false; } return true; @@ -475,11 +496,6 @@ namespace AZ bool HairFeatureProcessor::InitPPLLResolvePass() { m_hairPPLLResolvePass = nullptr; // reset it to null, just in case it fails to load the assets properly - if (!m_renderPipeline) - { - AZ_Error("Hair Gem", false, "Hair Fill Pass does NOT have render pipeline set yet"); - return false; - } RPI::Ptr desiredPass = m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairPPLLResolvePassName); if (desiredPass) @@ -489,12 +505,46 @@ namespace AZ } else { - AZ_Error("Hair Gem", false, "HairPPLLResolvePassTemplate does not have valid passes. Check your game project's .pass assets."); + AZ_Error("Hair Gem", false, "HairPPLLResolvePass cannot be found. Check your game project's .pass assets."); return false; } return true; } + //! Set the two short cut geometry pases and assign them the FP. The other two full screen passes + //! are generic full screen passes and don't need any interaction with the FP. + bool HairFeatureProcessor::InitShortCutRenderPasses() + { + m_hairShortCutGeometryDepthAlphaPass = nullptr; + m_hairShortCutGeometryShadingPass = nullptr; + + m_hairShortCutGeometryDepthAlphaPass = static_cast( + m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairShortCutGeometryDepthAlphaPassName).get()); + if (m_hairShortCutGeometryDepthAlphaPass) + { + m_hairShortCutGeometryDepthAlphaPass->SetFeatureProcessor(this); + } + else + { + AZ_Error("Hair Gem", false, "HairShortCutResolveDepthPass cannot be found. Check your game project's .pass assets."); + return false; + } + + m_hairShortCutGeometryShadingPass = static_cast( + m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairShortCutGeometryShadingPassName).get()); + if (m_hairShortCutGeometryShadingPass) + { + m_hairShortCutGeometryShadingPass->SetFeatureProcessor(this); + } + else + { + AZ_Error("Hair Gem", false, "HairShortCutGeometryShadingPass cannot be found. Check your game project's .pass assets."); + return false; + } + + return true; + } + void HairFeatureProcessor::BuildDispatchAndDrawItems(Data::Instance renderObject) { HairRenderObject* renderObjectPtr = renderObject.get(); @@ -513,9 +563,18 @@ namespace AZ m_computePasses[UpdateFollowHairPassName]->BuildDispatchItem( renderObjectPtr, DispatchLevel::DISPATCHLEVEL_VERTEX); - // Render / Raster pass - adding the object will schedule Srgs binding - // and DrawItem build. - m_hairPPLLRasterPass->SchedulePacketBuild(renderObjectPtr); + // Schedule Srgs binding and the DrawItem build. + // Since this does not bind the PerPass srg but prepare the rest of the Srgs + // such as the dynamic srg, it should only be done once per object per frame. + if (m_usePPLLRenderTechnique) + { + m_hairPPLLRasterPass->SchedulePacketBuild(renderObjectPtr); + } + else + { + m_hairShortCutGeometryDepthAlphaPass->SchedulePacketBuild(renderObjectPtr); + m_hairShortCutGeometryShadingPass->SchedulePacketBuild(renderObjectPtr); + } } Data::Instance HairFeatureProcessor::GetHairSkinningComputegPass() @@ -527,14 +586,28 @@ namespace AZ return m_computePasses[GlobalShapeConstraintsPassName]; } - Data::Instance HairFeatureProcessor::GetHairPPLLRasterPass() + Data::Instance HairFeatureProcessor::GetGeometryRasterShader() { - if (!m_hairPPLLRasterPass) + if (m_usePPLLRenderTechnique) { - Init(m_renderPipeline); + if (!m_hairPPLLRasterPass && !Init(m_renderPipeline)) + { + AZ_Error("Hair Gem", false, + "GetGeometryRasterShader - m_hairPPLLRasterPass was not created"); + return nullptr; + } + return m_hairPPLLRasterPass->GetShader(); } - return m_hairPPLLRasterPass; + + if (!m_hairShortCutGeometryDepthAlphaPass && !Init(m_renderPipeline)) + { + AZ_Error("Hair Gem", false, + "GetGeometryRasterShader - m_hairShortCutGeometryDepthAlphaPass was not created"); + return nullptr; + } + return m_hairShortCutGeometryDepthAlphaPass->GetShader(); } + } // namespace Hair } // namespace Render } // namespace AZ diff --git a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h index c56dc5c921..46660a6623 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h +++ b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h @@ -17,14 +17,19 @@ #include #include +#include // Hair specific #include #include + #include #include +#include +#include + #include #include #include @@ -73,9 +78,7 @@ namespace AZ { Name HairParentPassName; - Name HairPPLLRasterPassName; - Name HairPPLLResolvePassName; - + // Compute passes Name GlobalShapeConstraintsPassName; Name CalculateStrandDataPassName; Name VelocityShockPropagationPassName; @@ -83,6 +86,16 @@ namespace AZ Name LengthConstriantsWindAndCollisionPassName; Name UpdateFollowHairPassName; + // PPLL render passes + Name HairPPLLRasterPassName; + Name HairPPLLResolvePassName; + + // ShortCut render passes + Name HairShortCutGeometryDepthAlphaPassName; + Name HairShortCutResolveDepthPassName; + Name HairShortCutGeometryShadingPassName; + Name HairShortCutResolveColorPassName; + public: AZ_RTTI(AZ::Render::Hair::HairFeatureProcessor, "{5F9DDA81-B43F-4E30-9E56-C7C3DC517A4C}", RPI::FeatureProcessor); @@ -117,6 +130,7 @@ namespace AZ Data::Instance GetHairSkinningComputegPass(); Data::Instance GetHairPPLLRasterPass(); + Data::Instance GetGeometryRasterShader(); //! Update the hair objects materials array. void FillHairMaterialsArray(std::vector& renderSettings); @@ -144,6 +158,7 @@ namespace AZ bool InitPPLLFillPass(); bool InitPPLLResolvePass(); + bool InitShortCutRenderPasses(); bool InitComputePass(const Name& passName, bool allowIterations = false); void BuildDispatchAndDrawItems(Data::Instance renderObject); @@ -168,10 +183,14 @@ namespace AZ //! Simulation Compute Passes AZStd::unordered_map > m_computePasses; - // Render Passes + // PPLL Render Passes Data::Instance m_hairPPLLRasterPass = nullptr; Data::Instance m_hairPPLLResolvePass = nullptr; + // ShortCut Render Passes - special case for the geometry render passes + Data::Instance m_hairShortCutGeometryDepthAlphaPass = nullptr; + Data::Instance m_hairShortCutGeometryShadingPass = nullptr; + //-------------------------------------------------------------- // Per Pass Resources //-------------------------------------------------------------- @@ -196,6 +215,7 @@ namespace AZ bool m_forceClearRenderData = false; bool m_initialized = false; bool m_isEnabled = true; + bool m_usePPLLRenderTechnique = true; static uint32_t s_instanceCount; HairGlobalSettings m_hairGlobalSettings; diff --git a/Gems/AtomTressFX/Code/Rendering/HairRenderObject.cpp b/Gems/AtomTressFX/Code/Rendering/HairRenderObject.cpp index baf986daf6..4d615e31ae 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairRenderObject.cpp +++ b/Gems/AtomTressFX/Code/Rendering/HairRenderObject.cpp @@ -993,7 +993,7 @@ namespace AZ //------------------------------------- // Dynamic buffers, data and Srg creation - shared between passes and changed on the GPU if (!m_dynamicHairData.CreateDynamicGPUResources( - m_skinningShader, m_PPLLFillShader, + m_skinningShader, m_geometryRasterShader, m_NumTotalVertices, m_NumTotalStrands)) { AZ_Error("Hair Gem", false, "Hair - Error creating dynamic resources [%s]", assetName ); @@ -1028,7 +1028,7 @@ namespace AZ // Rendering setup bool renderResourcesSuccess; - renderResourcesSuccess = CreateRenderingGPUResources(m_PPLLFillShader, *asset, assetName); + renderResourcesSuccess = CreateRenderingGPUResources(m_geometryRasterShader, *asset, assetName); renderResourcesSuccess &= PopulateDrawStrandsBindSet(renderSettings); renderResourcesSuccess &= UploadRenderingGPUResources(*asset); @@ -1057,17 +1057,10 @@ namespace AZ } { - Data::Instance rasterPass = m_featureProcessor->GetHairPPLLRasterPass(); - if (!rasterPass.get()) + m_geometryRasterShader = m_featureProcessor->GetGeometryRasterShader(); + if (!m_geometryRasterShader) { - AZ_Error("Hair Gem", false, "Failed to get PPLL raster fill Pass."); - return false; - } - - m_PPLLFillShader = rasterPass->GetShader(); - if (!m_PPLLFillShader) - { - AZ_Error("Hair Gem", false, "Failed to get hair raster fill shader from raster pass"); + AZ_Error("Hair Gem", false, "Failed to get hair geometry raster shader"); return false; } } @@ -1116,7 +1109,7 @@ namespace AZ return updatedCB; } - bool HairRenderObject::BuildPPLLDrawPacket(RHI::DrawPacketBuilder::DrawRequest& drawRequest) + bool HairRenderObject::BuildDrawPacket(RPI::Shader* geometryShader, RHI::DrawPacketBuilder::DrawRequest& drawRequest) { RHI::DrawPacketBuilder drawPacketBuilder; RHI::DrawIndexed drawIndexed; @@ -1159,21 +1152,38 @@ namespace AZ drawPacketBuilder.AddShaderResourceGroup(simSrg->GetRHIShaderResourceGroup()); drawPacketBuilder.AddDrawItem(drawRequest); - if (m_fillDrawPacket) + const RHI::DrawPacket* drawPacket = drawPacketBuilder.End(); + if (!drawPacket) { - delete m_fillDrawPacket; + AZ_Error("Hair Gem", false, "Failed to build the hair DrawPacket."); + return false; } - m_fillDrawPacket = drawPacketBuilder.End(); - if (!m_fillDrawPacket) + // Insert the newly created draw packet to the map based on its shader + auto iter = m_geometryDrawPackets.find(geometryShader); + if (iter != m_geometryDrawPackets.end()) { - AZ_Error("Hair Gem", false, "Failed to build the hair DrawPacket."); - return false; + delete iter->second; + iter->second = drawPacket; + } + else + { + m_geometryDrawPackets[geometryShader] = drawPacket; } return true; } + const RHI::DrawPacket* HairRenderObject::GetGeometrylDrawPacket(RPI::Shader* geometryShader) + { + auto iter = m_geometryDrawPackets.find(geometryShader); + if (iter == m_geometryDrawPackets.end()) + { + return nullptr; + } + return iter->second; + } + const RHI::DispatchItem* HairRenderObject::GetDispatchItem(RPI::Shader* computeShader) { auto dispatchIter = m_dispatchItems.find(computeShader); @@ -1210,4 +1220,3 @@ namespace AZ } // namespace Hair } // namespace Render } // namespace AZ - diff --git a/Gems/AtomTressFX/Code/Rendering/HairRenderObject.h b/Gems/AtomTressFX/Code/Rendering/HairRenderObject.h index fa4095eed0..817ebd89fa 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairRenderObject.h +++ b/Gems/AtomTressFX/Code/Rendering/HairRenderObject.h @@ -179,14 +179,9 @@ namespace AZ AMD::TressFXSimulationSettings* simSettings, AMD::TressFXRenderingSettings* renderSettings ); - //! Creates and fill the draw item associated with the PPLL render of the - //! current hair object - const RHI::DrawPacket* GetFillDrawPacket() - { - return m_fillDrawPacket; - } + bool BuildDrawPacket(RPI::Shader* geometryShader, RHI::DrawPacketBuilder::DrawRequest& drawRequest); - bool BuildPPLLDrawPacket(RHI::DrawPacketBuilder::DrawRequest& drawRequest); + const RHI::DrawPacket* GetGeometrylDrawPacket(RPI::Shader* geometryShader); //! Creates and fill the dispatch item associated with the compute shader bool BuildDispatchItem(RPI::Shader* computeShader, DispatchLevel dispatchLevel); @@ -302,17 +297,20 @@ namespace AZ //! responsible for the various stages and passes' updates HairFeatureProcessor* m_featureProcessor = nullptr; - //! The dispatch item used for the skinning - HairDispatchItem m_skinningDispatchItem; + //! Skinning compute shader used for creation of the compute Srgs and dispatch item + Data::Instance m_skinningShader = nullptr; //! Compute dispatch items map per the existing passes - AZStd::map> m_dispatchItems; + AZStd::unordered_map> m_dispatchItems; - //! Skinning compute shader used for creation of the compute Srgs and dispatch item - Data::Instance m_skinningShader = nullptr; + //! Geometry raster shader used for creation of the raster Srgs. + //! Since the Srgs for geometry raster are the same across the shaders we keep + //! only a single shader - if this to change in the future, several shaders and sets + //! of dynamic Srgs should be created. + Data::Instance m_geometryRasterShader = nullptr; - //! PPLL fill shader used for creation of the raster Srgs and draw item - Data::Instance m_PPLLFillShader = nullptr; + //! DrawPacket for the multi object geometry raster pass. + AZStd::unordered_map m_geometryDrawPackets; float m_frameDeltaTime = 0.02; @@ -378,9 +376,6 @@ namespace AZ //! Index buffer for the render pass via draw calls - naming was kept Data::Instance m_indexBuffer; RHI::IndexBufferView m_indexBufferView; - - //! DrawPacket for the multi object raster fill pass. - const RHI::DrawPacket* m_fillDrawPacket = nullptr; //------------------------------------------------------------------- AZStd::mutex m_mutex; diff --git a/Gems/AtomTressFX/Hair_files.cmake b/Gems/AtomTressFX/Hair_files.cmake index 000abeb559..180685d499 100644 --- a/Gems/AtomTressFX/Hair_files.cmake +++ b/Gems/AtomTressFX/Hair_files.cmake @@ -67,6 +67,13 @@ set(FILES # Base class of all geometry raster passes Code/Passes/HairGeometryRasterPass.h Code/Passes/HairGeometryRasterPass.cpp + + # ShortCut rendering technique - pass classes + Code/Passes/HairShortCutGeometryDepthAlphaPass.h + Code/Passes/HairShortCutGeometryDepthAlphaPass.cpp + Code/Passes/HairShortCutGeometryShadingPass.h + Code/Passes/HairShortCutGeometryShadingPass.cpp + # PPLL rendering technique - geometry raster pass Code/Passes/HairPPLLRasterPass.h Code/Passes/HairPPLLRasterPass.cpp @@ -84,30 +91,37 @@ set(FILES Code/Assets/HairAsset.cpp #) #set(shaders_sources - # Srgs and Utility files - Assets/Shaders/HairSrgs.azsli - Assets/Shaders/HairSimulationSrgs.azsli + # Geometry and Full Screen azsl utility files Assets/Shaders/HairRenderingSrgs.azsli - Assets/Shaders/HairSimulationCommon.azsli Assets/Shaders/HairStrands.azsli Assets/Shaders/HairUtilities.azsli + Assets/Shaders/HairFullScreenUtils.azsli Assets/Shaders/HairLighting.azsli Assets/Shaders/HairLightingEquations.azsli Assets/Shaders/HairLightTypes.azsli Assets/Shaders/HairSurface.azsli - # Simulation Compute shaders + # ShortCut technique shaders (using multiple RTs instead of PPLL for GPU memory reduction) + Assets/Shaders/HairShortCutGeometryDepthAlpha.azsl + Assets/Shaders/HairShortCutResolveDepth.azsl + Assets/Shaders/HairShortCutGeometryShading.azsl + Assets/Shaders/HairShortCutResolveColor.azsl + + # Rendering azsl files + Assets/Shaders/HairRenderingFillPPLL.azsl + Assets/Shaders/HairRenderingResolvePPLL.azsl + + # Simulation Compute azsl files + Assets/Shaders/HairComputeSrgs.azsli + Assets/Shaders/HairSimulationComputeSrgs.azsli + Assets/Shaders/HairSimulationCommon.azsli Assets/Shaders/HairSimulationCompute.azsl - # Collision shaders - to be included soon + # Collision azsl files - to be included soon # Assets/Shaders/HairCollisionPrepareSDF.azsl # Assets/Shaders/HairCollisionWithSDF.azsl - # Rendering shaders - Assets/Shaders/HairRenderingFillPPLL.azsl - Assets/Shaders/HairRenderingResolvePPLL.azsl - - # Simulation .shader files + # Simulation Compute .shader files Assets/Shaders/HairGlobalShapeConstraintsCompute.shader Assets/Shaders/HairCalculateStrandLevelDataCompute.shader Assets/Shaders/HairVelocityShockPropagationCompute.shader @@ -115,9 +129,15 @@ set(FILES Assets/Shaders/HairLengthConstraintsWindAndCollisionCompute.shader Assets/Shaders/HairUpdateFollowHairCompute.shader - # Rendering .shader file + # PPLL Render .shader file Assets/Shaders/HairRenderingFillPPLL.shader Assets/Shaders/HairRenderingResolvePPLL.shader + + # ShortCut Render .shader file + Assets/Shaders/HairShortCutGeometryDepthAlpha.shader + Assets/Shaders/HairShortCutResolveDepth.shader + Assets/Shaders/HairShortCutGeometryShading.shader + Assets/Shaders/HairShortCutResolveColor.shader # Colisions .shader files - to be included soon # Assets/Shaders/HairCollisionInitializeSDF.shader @@ -127,15 +147,25 @@ set(FILES #) # #set(atom_hair_passes + # Compute simulation and skinning passes Assets/Passes/HairParentPass.pass + Assets/Passes/HairParentShortCutPass.pass Assets/Passes/HairGlobalShapeConstraintsCompute.pass Assets/Passes/HairCalculateStrandLevelDataCompute.pass Assets/Passes/HairVelocityShockPropagationCompute.pass Assets/Passes/HairLocalShapeConstraintsCompute.pass Assets/Passes/HairLengthConstraintsWindAndCollisionCompute.pass Assets/Passes/HairUpdateFollowHairCompute.pass + + # PPLL render passes Assets/Passes/HairFillPPLL.pass Assets/Passes/HairResolvePPLL.pass + + # Shortcut render passes + Assets/Passes/HairShortCutGeometryDepthAlpha.pass + Assets/Passes/HairShortCutResolveDepth.pass + Assets/Passes/HairShortCutGeometryShading.pass + Assets/Passes/HairShortCutResolveColor.pass ) set(SKIP_UNITY_BUILD_INCLUSION_FILES diff --git a/Gems/AudioEngineWwise/Code/CMakeLists.txt b/Gems/AudioEngineWwise/Code/CMakeLists.txt index 98a31fb91a..33c80bc31c 100644 --- a/Gems/AudioEngineWwise/Code/CMakeLists.txt +++ b/Gems/AudioEngineWwise/Code/CMakeLists.txt @@ -18,31 +18,18 @@ set(AUDIOENGINEWWISE_COMPILEDEFINITIONS find_package(Wwise MODULE) ################################################################################ -# Server / Unsupported +# Servers +# (and situations where Wwise SDK is not found or otherwise unavailable) ################################################################################ -if (PAL_TRAIT_BUILD_SERVER_SUPPORTED OR PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB OR NOT Wwise_FOUND) - # Stub gem for server and unsupported platforms. Audio Engine Wwise is client only - ly_add_target( - NAME AudioEngineWwise.Stub ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} - NAMESPACE Gem - FILES_CMAKE - audioenginewwise_stub_files.cmake - BUILD_DEPENDENCIES - PRIVATE - AZ::AzCore - ) -endif() - -if (PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB OR NOT Wwise_FOUND) - # setup aliases so stubs will be used if something references AudioEngineWwise(.Editor) - add_library(Gem::AudioEngineWwise ALIAS AudioEngineWwise.Stub) - add_library(Gem::AudioEngineWwise.Editor ALIAS AudioEngineWwise.Stub) +if(NOT PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED OR NOT Wwise_FOUND) + # Don't create any Gem targets and aliases. Nothing should depend on this + # Gem directly, because if it doesn't define targets it will cause an error. return() endif() ################################################################################ -# Runtime / Game +# Clients ################################################################################ ly_add_target( NAME AudioEngineWwise.Static STATIC @@ -181,7 +168,7 @@ if (PAL_TRAIT_BUILD_TESTS_SUPPORTED) endif() ################################################################################ -# Tools / Editor +# Tools / Builders ################################################################################ if (PAL_TRAIT_BUILD_HOST_TOOLS) ly_add_target( diff --git a/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake b/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake b/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake b/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake b/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake b/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/Blast/Code/Tests/Mocks/BlastMocks.h b/Gems/Blast/Code/Tests/Mocks/BlastMocks.h index 1fe6e35d75..949d3add0d 100644 --- a/Gems/Blast/Code/Tests/Mocks/BlastMocks.h +++ b/Gems/Blast/Code/Tests/Mocks/BlastMocks.h @@ -209,6 +209,7 @@ namespace Blast AZStd::shared_ptr( const Physics::ColliderConfiguration&, const Physics::ShapeConfiguration&)); MOCK_METHOD1(ReleaseNativeMeshObject, void(void*)); + MOCK_METHOD1(ReleaseNativeHeightfieldObject, void(void*)); MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr(const Physics::MaterialConfiguration&)); MOCK_METHOD0(GetDefaultMaterial, AZStd::shared_ptr()); MOCK_METHOD1( diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphMotionNode.cpp b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphMotionNode.cpp index c5e50ed9dd..a0a811eff0 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphMotionNode.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphMotionNode.cpp @@ -597,7 +597,7 @@ namespace EMotionFX // reset several settings to rewind the motion instance motionInstance->ResetTimes(); motionInstance->SetIsFrozen(false); - SetSyncIndex(animGraphInstance, MCORE_INVALIDINDEX32); + SetSyncIndex(animGraphInstance, InvalidIndex); uniqueData->SetCurrentPlayTime(motionInstance->GetCurrentTime()); uniqueData->SetDuration(motionInstance->GetDuration()); uniqueData->SetPreSyncTime(uniqueData->GetCurrentPlayTime()); diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNode.h b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNode.h index b515621c5b..95d9c0d77d 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNode.h +++ b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNode.h @@ -213,12 +213,12 @@ namespace EMotionFX */ virtual void SkipOutput([[maybe_unused]] AnimGraphInstance* animGraphInstance) {} - MCORE_INLINE float GetDuration(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetDuration(); } + float GetDuration(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetDuration(); } virtual void SetCurrentPlayTime(AnimGraphInstance* animGraphInstance, float timeInSeconds) { FindOrCreateUniqueNodeData(animGraphInstance)->SetCurrentPlayTime(timeInSeconds); } virtual float GetCurrentPlayTime(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetCurrentPlayTime(); } - MCORE_INLINE size_t GetSyncIndex(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetSyncIndex(); } - MCORE_INLINE void SetSyncIndex(AnimGraphInstance* animGraphInstance, size_t syncIndex) { FindOrCreateUniqueNodeData(animGraphInstance)->SetSyncIndex(syncIndex); } + size_t GetSyncIndex(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetSyncIndex(); } + void SetSyncIndex(AnimGraphInstance* animGraphInstance, size_t syncIndex) { FindOrCreateUniqueNodeData(animGraphInstance)->SetSyncIndex(syncIndex); } virtual void SetPlaySpeed(AnimGraphInstance* animGraphInstance, float speedFactor) { FindOrCreateUniqueNodeData(animGraphInstance)->SetPlaySpeed(speedFactor); } virtual float GetPlaySpeed(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetPlaySpeed(); } diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNodeData.h b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNodeData.h index 1ad82b1d55..5c2f0b281b 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNodeData.h +++ b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNodeData.h @@ -51,52 +51,52 @@ namespace EMotionFX void Init(AnimGraphInstance* animGraphInstance, AnimGraphNode* node); void Init(AnimGraphNodeData* nodeData); - MCORE_INLINE AnimGraphNode* GetNode() const { return reinterpret_cast(m_object); } - MCORE_INLINE void SetNode(AnimGraphNode* node) { m_object = reinterpret_cast(node); } + AnimGraphNode* GetNode() const { return reinterpret_cast(m_object); } + void SetNode(AnimGraphNode* node) { m_object = reinterpret_cast(node); } - MCORE_INLINE void SetSyncIndex(size_t syncIndex) { m_syncIndex = syncIndex; } - MCORE_INLINE size_t GetSyncIndex() const { return m_syncIndex; } + void SetSyncIndex(size_t syncIndex) { m_syncIndex = syncIndex; } + size_t GetSyncIndex() const { return m_syncIndex; } - MCORE_INLINE void SetCurrentPlayTime(float absoluteTime) { m_currentTime = absoluteTime; } - MCORE_INLINE float GetCurrentPlayTime() const { return m_currentTime; } + void SetCurrentPlayTime(float absoluteTime) { m_currentTime = absoluteTime; } + float GetCurrentPlayTime() const { return m_currentTime; } - MCORE_INLINE void SetPlaySpeed(float speed) { m_playSpeed = speed; } - MCORE_INLINE float GetPlaySpeed() const { return m_playSpeed; } + void SetPlaySpeed(float speed) { m_playSpeed = speed; } + float GetPlaySpeed() const { return m_playSpeed; } - MCORE_INLINE void SetDuration(float durationInSeconds) { m_duration = durationInSeconds; } - MCORE_INLINE float GetDuration() const { return m_duration; } + void SetDuration(float durationInSeconds) { m_duration = durationInSeconds; } + float GetDuration() const { return m_duration; } - MCORE_INLINE void SetPreSyncTime(float timeInSeconds) { m_preSyncTime = timeInSeconds; } - MCORE_INLINE float GetPreSyncTime() const { return m_preSyncTime; } + void SetPreSyncTime(float timeInSeconds) { m_preSyncTime = timeInSeconds; } + float GetPreSyncTime() const { return m_preSyncTime; } - MCORE_INLINE void SetGlobalWeight(float weight) { m_globalWeight = weight; } - MCORE_INLINE float GetGlobalWeight() const { return m_globalWeight; } + void SetGlobalWeight(float weight) { m_globalWeight = weight; } + float GetGlobalWeight() const { return m_globalWeight; } - MCORE_INLINE void SetLocalWeight(float weight) { m_localWeight = weight; } - MCORE_INLINE float GetLocalWeight() const { return m_localWeight; } + void SetLocalWeight(float weight) { m_localWeight = weight; } + float GetLocalWeight() const { return m_localWeight; } - MCORE_INLINE uint8 GetInheritFlags() const { return m_inheritFlags; } + uint8 GetInheritFlags() const { return m_inheritFlags; } - MCORE_INLINE bool GetIsBackwardPlaying() const { return (m_inheritFlags & INHERITFLAGS_BACKWARD) != 0; } - MCORE_INLINE void SetBackwardFlag() { m_inheritFlags |= INHERITFLAGS_BACKWARD; } - MCORE_INLINE void ClearInheritFlags() { m_inheritFlags = 0; } + bool GetIsBackwardPlaying() const { return (m_inheritFlags & INHERITFLAGS_BACKWARD) != 0; } + void SetBackwardFlag() { m_inheritFlags |= INHERITFLAGS_BACKWARD; } + void ClearInheritFlags() { m_inheritFlags = 0; } - MCORE_INLINE uint8 GetPoseRefCount() const { return m_poseRefCount; } - MCORE_INLINE void IncreasePoseRefCount() { m_poseRefCount++; } - MCORE_INLINE void DecreasePoseRefCount() { m_poseRefCount--; } - MCORE_INLINE void SetPoseRefCount(uint8 refCount) { m_poseRefCount = refCount; } + uint8 GetPoseRefCount() const { return m_poseRefCount; } + void IncreasePoseRefCount() { m_poseRefCount++; } + void DecreasePoseRefCount() { m_poseRefCount--; } + void SetPoseRefCount(uint8 refCount) { m_poseRefCount = refCount; } - MCORE_INLINE uint8 GetRefDataRefCount() const { return m_refDataRefCount; } - MCORE_INLINE void IncreaseRefDataRefCount() { m_refDataRefCount++; } - MCORE_INLINE void DecreaseRefDataRefCount() { m_refDataRefCount--; } - MCORE_INLINE void SetRefDataRefCount(uint8 refCount) { m_refDataRefCount = refCount; } + uint8 GetRefDataRefCount() const { return m_refDataRefCount; } + void IncreaseRefDataRefCount() { m_refDataRefCount++; } + void DecreaseRefDataRefCount() { m_refDataRefCount--; } + void SetRefDataRefCount(uint8 refCount) { m_refDataRefCount = refCount; } - MCORE_INLINE void SetRefCountedData(AnimGraphRefCountedData* data) { m_refCountedData = data; } - MCORE_INLINE AnimGraphRefCountedData* GetRefCountedData() const { return m_refCountedData; } + void SetRefCountedData(AnimGraphRefCountedData* data) { m_refCountedData = data; } + AnimGraphRefCountedData* GetRefCountedData() const { return m_refCountedData; } - MCORE_INLINE const AnimGraphSyncTrack* GetSyncTrack() const { return m_syncTrack; } - MCORE_INLINE AnimGraphSyncTrack* GetSyncTrack() { return m_syncTrack; } - MCORE_INLINE void SetSyncTrack(AnimGraphSyncTrack* syncTrack) { m_syncTrack = syncTrack; } + const AnimGraphSyncTrack* GetSyncTrack() const { return m_syncTrack; } + AnimGraphSyncTrack* GetSyncTrack() { return m_syncTrack; } + void SetSyncTrack(AnimGraphSyncTrack* syncTrack) { m_syncTrack = syncTrack; } bool GetIsMirrorMotion() const { return m_isMirrorMotion; } void SetIsMirrorMotion(bool newValue) { m_isMirrorMotion = newValue; } diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphStateTransition.cpp b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphStateTransition.cpp index e90491b696..c6d9b4eeec 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphStateTransition.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphStateTransition.cpp @@ -326,7 +326,7 @@ namespace EMotionFX uniqueData->m_totalSeconds = 0.0f; uniqueData->m_blendProgress = 0.0f; - m_targetNode->SetSyncIndex(animGraphInstance, MCORE_INVALIDINDEX32); + m_targetNode->SetSyncIndex(animGraphInstance, InvalidIndex); // Trigger action for (AnimGraphTriggerAction* action : m_actionSetup.GetActions()) diff --git a/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h b/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h index 882d198092..6649cea55e 100644 --- a/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h +++ b/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h @@ -46,6 +46,7 @@ namespace Physics } MOCK_METHOD2(CreateShape, AZStd::shared_ptr(const Physics::ColliderConfiguration& colliderConfiguration, const Physics::ShapeConfiguration& configuration)); MOCK_METHOD1(ReleaseNativeMeshObject, void(void* nativeMeshObject)); + MOCK_METHOD1(ReleaseNativeHeightfieldObject, void(void* nativeMeshObject)); MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr(const Physics::MaterialConfiguration& materialConfiguration)); MOCK_METHOD3(CookConvexMeshToFile, bool(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount)); MOCK_METHOD3(CookConvexMeshToMemory, bool(const AZ::Vector3* vertices, AZ::u32 vertexCount, AZStd::vector& result)); diff --git a/Gems/GradientSignal/Code/CMakeLists.txt b/Gems/GradientSignal/Code/CMakeLists.txt index 64f565cf99..5b88044116 100644 --- a/Gems/GradientSignal/Code/CMakeLists.txt +++ b/Gems/GradientSignal/Code/CMakeLists.txt @@ -107,7 +107,16 @@ endif() # Tests ################################################################################ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) - + ly_add_target( + NAME GradientSignal.Mocks HEADERONLY + NAMESPACE Gem + FILES_CMAKE + gradientsignal_mocks_files.cmake + INCLUDE_DIRECTORIES + INTERFACE + Mocks + ) + ly_add_target( NAME GradientSignal.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} NAMESPACE Gem @@ -122,6 +131,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) AZ::AzTest Gem::GradientSignal.Static Gem::LmbrCentral + Gem::GradientSignal.Mocks ) ly_add_googletest( NAME Gem::GradientSignal.Tests diff --git a/Gems/GradientSignal/Code/Mocks/GradientSignal/Ebuses/MockGradientRequestBus.h b/Gems/GradientSignal/Code/Mocks/GradientSignal/Ebuses/MockGradientRequestBus.h new file mode 100644 index 0000000000..69e96268f6 --- /dev/null +++ b/Gems/GradientSignal/Code/Mocks/GradientSignal/Ebuses/MockGradientRequestBus.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 + +#include +#include +#include + +namespace UnitTest +{ + class MockGradientRequests + : private GradientSignal::GradientRequestBus::Handler + { + public: + MockGradientRequests(AZ::EntityId entityId) + { + GradientSignal::GradientRequestBus::Handler::BusConnect(entityId); + } + + ~MockGradientRequests() + { + GradientSignal::GradientRequestBus::Handler::BusDisconnect(); + } + + MOCK_CONST_METHOD1(GetValue, float(const GradientSignal::GradientSampleParams&)); + MOCK_CONST_METHOD1(IsEntityInHierarchy, bool(const AZ::EntityId&)); + }; +} // namespace UnitTest + diff --git a/Gems/GradientSignal/Code/gradientsignal_mocks_files.cmake b/Gems/GradientSignal/Code/gradientsignal_mocks_files.cmake new file mode 100644 index 0000000000..82c8c3793f --- /dev/null +++ b/Gems/GradientSignal/Code/gradientsignal_mocks_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 + Mocks/GradientSignal/Ebuses/MockGradientRequestBus.h +) diff --git a/Gems/GraphCanvas/Code/Source/Components/Connections/ConnectionComponent.cpp b/Gems/GraphCanvas/Code/Source/Components/Connections/ConnectionComponent.cpp index 690026e477..d61e8bda29 100644 --- a/Gems/GraphCanvas/Code/Source/Components/Connections/ConnectionComponent.cpp +++ b/Gems/GraphCanvas/Code/Source/Components/Connections/ConnectionComponent.cpp @@ -13,6 +13,8 @@ #include +#include + #include #include @@ -1117,10 +1119,8 @@ namespace GraphCanvas QPointF globalConnectionPoint = ConversionUtils::AZToQPoint(globalConnectionVector); QPointF anchorPoint(0.0f, 0.0f); - - ToastConfiguration toastConfiguration(ToastType::Error, "Unable to connect to slot", m_validationResult.m_failureReason); - - toastConfiguration.SetCloseOnClick(false); + AzQtComponents::ToastConfiguration toastConfiguration(AzQtComponents::ToastType::Error, "Unable to connect to slot", m_validationResult.m_failureReason.c_str()); + toastConfiguration.m_closeOnClick = false; m_toastId = viewHandler->ShowToastAtPoint(globalConnectionPoint.toPoint(), anchorPoint, toastConfiguration); } diff --git a/Gems/GraphCanvas/Code/Source/Components/Connections/ConnectionComponent.h b/Gems/GraphCanvas/Code/Source/Components/Connections/ConnectionComponent.h index dc35772b87..b447cee394 100644 --- a/Gems/GraphCanvas/Code/Source/Components/Connections/ConnectionComponent.h +++ b/Gems/GraphCanvas/Code/Source/Components/Connections/ConnectionComponent.h @@ -19,6 +19,7 @@ AZ_POP_DISABLE_WARNING #include #include #include +#include #include #include @@ -210,7 +211,7 @@ namespace GraphCanvas ConnectionValidationTooltip m_validationResult; Endpoint m_endpointTooltip; - ToastId m_toastId; + AzToolsFramework::ToastId m_toastId; //! The Id of the graph this connection belongs to. GraphId m_graphId; diff --git a/Gems/GraphCanvas/Code/Source/Components/Slots/Data/DataSlotLayoutComponent.cpp b/Gems/GraphCanvas/Code/Source/Components/Slots/Data/DataSlotLayoutComponent.cpp index 95c6ecff11..78a94296ff 100644 --- a/Gems/GraphCanvas/Code/Source/Components/Slots/Data/DataSlotLayoutComponent.cpp +++ b/Gems/GraphCanvas/Code/Source/Components/Slots/Data/DataSlotLayoutComponent.cpp @@ -12,6 +12,8 @@ #include #include +#include + #include #include @@ -157,8 +159,8 @@ namespace GraphCanvas anchorPoint = QPointF(1.0f, 0.5f); } - ToastConfiguration toastConfiguration(ToastType::Error, "Unable to drop onto to slot", error); - toastConfiguration.SetCloseOnClick(false); + AzQtComponents::ToastConfiguration toastConfiguration(AzQtComponents::ToastType::Error, "Unable to drop onto to slot", error.c_str()); + toastConfiguration.m_closeOnClick = false; m_toastId = viewHandler->ShowToastAtPoint(globalConnectionPoint.toPoint(), anchorPoint, toastConfiguration); } diff --git a/Gems/GraphCanvas/Code/Source/Components/Slots/Data/DataSlotLayoutComponent.h b/Gems/GraphCanvas/Code/Source/Components/Slots/Data/DataSlotLayoutComponent.h index 3c49b36833..61fd6f31db 100644 --- a/Gems/GraphCanvas/Code/Source/Components/Slots/Data/DataSlotLayoutComponent.h +++ b/Gems/GraphCanvas/Code/Source/Components/Slots/Data/DataSlotLayoutComponent.h @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -58,7 +59,7 @@ namespace GraphCanvas SlotId m_slotId; ViewId m_viewId; - ToastId m_toastId; + AzToolsFramework::ToastId m_toastId; }; class DoubleClickSceneEventFilter diff --git a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Components/ToastBus.h b/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Components/ToastBus.h deleted file mode 100644 index b775c5f068..0000000000 --- a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Components/ToastBus.h +++ /dev/null @@ -1,29 +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 - * - */ -#pragma once - -#include - -#include - -namespace GraphCanvas -{ - class ToastNotifications - : public AZ::EBusTraits - { - public: - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById; - using BusIdType = ToastId; - - virtual void OnToastInteraction() {} - virtual void OnToastDismissed() {} - }; - - using ToastNotificationBus = AZ::EBus; -} diff --git a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Components/ViewBus.h b/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Components/ViewBus.h index 590b3812a8..05bf650dc1 100644 --- a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Components/ViewBus.h +++ b/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Components/ViewBus.h @@ -12,6 +12,7 @@ #include #include #include +#include #include @@ -139,11 +140,11 @@ namespace GraphCanvas virtual void RefreshView() = 0; //! Toast Notifications - virtual void HideToastNotification(const ToastId& toastId) = 0; + virtual void HideToastNotification(const AzToolsFramework::ToastId& toastId) = 0; - virtual ToastId ShowToastNotification(const ToastConfiguration& toastConfiguration) = 0; - virtual ToastId ShowToastAtCursor(const ToastConfiguration& toastConfiguration) = 0; - virtual ToastId ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const ToastConfiguration&) = 0; + virtual AzToolsFramework::ToastId ShowToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration) = 0; + virtual AzToolsFramework::ToastId ShowToastAtCursor(const AzQtComponents::ToastConfiguration& toastConfiguration) = 0; + virtual AzToolsFramework::ToastId ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const AzQtComponents::ToastConfiguration&) = 0; virtual bool IsShowing() const = 0; }; diff --git a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Editor/EditorTypes.h b/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Editor/EditorTypes.h index e0722836b1..792e02bd4b 100644 --- a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Editor/EditorTypes.h +++ b/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Editor/EditorTypes.h @@ -29,8 +29,6 @@ namespace GraphCanvas typedef AZ::EntityId GraphicsEffectId; - typedef AZ::EntityId ToastId; - typedef AZ::Uuid PersistentGraphMemberId; typedef AZ::Crc32 ExtenderId; @@ -81,14 +79,6 @@ namespace GraphCanvas Target }; - enum class ToastType - { - Information, - Warning, - Error, - Custom - }; - enum class ListingType { Unknown, @@ -116,88 +106,6 @@ namespace GraphCanvas private: }; - class ToastConfiguration - { - public: - ToastConfiguration(ToastType toastType, const AZStd::string& titleLabel, const AZStd::string& descriptionLabel) - : m_toastType(toastType) - , m_titleLabel(titleLabel) - , m_descriptionLabel(descriptionLabel) - { - } - - ~ToastConfiguration() = default; - - ToastType GetToastType() const - { - return m_toastType; - } - - const AZStd::string& GetTitleLabel() const - { - return m_titleLabel; - } - - const AZStd::string& GetDescriptionLabel() const - { - return m_descriptionLabel; - } - - void SetCustomToastImage(const AZStd::string& toastImage) - { - AZ_Error("GraphCanvas", m_toastType == ToastType::Custom, "Setting a custom image on a non-custom Toast notification"); - m_customToastImage = toastImage; - } - - const AZStd::string& GetCustomToastImage() const - { - return m_customToastImage; - } - - void SetDuration(AZStd::chrono::milliseconds duration) - { - m_duration = duration; - } - - AZStd::chrono::milliseconds GetDuration() const - { - return m_duration; - } - - void SetCloseOnClick(bool closeOnClick) - { - m_closeOnClick = closeOnClick; - } - - bool GetCloseOnClick() const - { - return m_closeOnClick; - } - - void SetFadeDuration(AZStd::chrono::milliseconds fadeDuration) - { - m_fadeDuration = fadeDuration; - } - - AZStd::chrono::milliseconds GetFadeDuration() const - { - return m_fadeDuration; - } - - private: - - AZStd::chrono::milliseconds m_fadeDuration = AZStd::chrono::milliseconds(250); - - AZStd::chrono::milliseconds m_duration; - bool m_closeOnClick; - - AZStd::string m_customToastImage; - - ToastType m_toastType; - AZStd::string m_titleLabel; - AZStd::string m_descriptionLabel; - }; - struct ConnectionValidationTooltip { bool operator()() const diff --git a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/GraphCanvasGraphicsView/GraphCanvasGraphicsView.cpp b/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/GraphCanvasGraphicsView/GraphCanvasGraphicsView.cpp index fa7e051035..a8b0f7dabf 100644 --- a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/GraphCanvasGraphicsView/GraphCanvasGraphicsView.cpp +++ b/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/GraphCanvasGraphicsView/GraphCanvasGraphicsView.cpp @@ -12,6 +12,7 @@ #include #include +#include #include @@ -46,6 +47,7 @@ namespace GraphCanvas , m_queuedFocus(nullptr) { m_viewId = AZ::Entity::MakeId(); + m_notificationsView = AZStd::make_unique(this, AZ_CRC(m_viewId.ToString())); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); @@ -603,8 +605,8 @@ namespace GraphCanvas const int maxSize = 17500; if (windowSize.width() > maxSize || windowSize.height() > maxSize) { - ToastConfiguration toastConfiguration(ToastType::Information, "Screenshot", "Screenshot attempted to capture an area too large. Some down-ressing may occur."); - ShowToastNotification(toastConfiguration); + AzQtComponents::ToastConfiguration toastConfiguration(AzQtComponents::ToastType::Information, "Screenshot", "Screenshot attempted to capture an area too large. Some down-ressing may occur."); + m_notificationsView->ShowToastNotification(toastConfiguration); if (windowSize.width() > maxSize) { @@ -825,64 +827,24 @@ namespace GraphCanvas invalidateScene(GetViewableAreaInSceneCoordinates()); } - void GraphCanvasGraphicsView::HideToastNotification(const ToastId& toastId) + void GraphCanvasGraphicsView::HideToastNotification(const AzToolsFramework::ToastId& toastId) { - auto notificationIter = m_notifications.find(toastId); - - if (notificationIter != m_notifications.end()) - { - auto queuedIter = AZStd::find(m_queuedNotifications.begin(), m_queuedNotifications.end(), toastId); - - if (queuedIter != m_queuedNotifications.end()) - { - m_queuedNotifications.erase(queuedIter); - } - - notificationIter->second->reject(); - } + m_notificationsView->HideToastNotification(toastId); } - ToastId GraphCanvasGraphicsView::ShowToastNotification(const ToastConfiguration& toastConfiguration) + AzToolsFramework::ToastId GraphCanvasGraphicsView::ShowToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration) { - ToastNotification* notification = aznew ToastNotification(this, toastConfiguration); - - ToastId toastId = notification->GetToastId(); - m_notifications[toastId] = notification; - - m_queuedNotifications.emplace_back(toastId); - - if (!m_activeNotification.IsValid()) - { - DisplayQueuedNotification(); - } - - return toastId; + return m_notificationsView->ShowToastNotification(toastConfiguration); } - ToastId GraphCanvasGraphicsView::ShowToastAtCursor(const ToastConfiguration& toastConfiguration) + AzToolsFramework::ToastId GraphCanvasGraphicsView::ShowToastAtCursor(const AzQtComponents::ToastConfiguration& toastConfiguration) { - ToastNotification* notification = aznew ToastNotification(this, toastConfiguration); - - notification->ShowToastAtCursor(); - - ToastId toastId = notification->GetToastId(); - - m_notifications[toastId] = notification; - - return toastId; + return m_notificationsView->ShowToastAtCursor(toastConfiguration); } - ToastId GraphCanvasGraphicsView::ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const ToastConfiguration& toastConfiguration) + AzToolsFramework::ToastId GraphCanvasGraphicsView::ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const AzQtComponents::ToastConfiguration& toastConfiguration) { - ToastNotification* notification = aznew ToastNotification(this, toastConfiguration); - - notification->ShowToastAtPoint(screenPosition, anchorPoint); - - ToastId toastId = notification->GetToastId(); - - m_notifications[toastId] = notification; - - return toastId; + return m_notificationsView->ShowToastAtPoint(screenPosition, anchorPoint, toastConfiguration); } bool GraphCanvasGraphicsView::IsShowing() const @@ -1245,14 +1207,14 @@ namespace GraphCanvas CalculateInternalRectangle(); - UpdateToastPosition(); + m_notificationsView->UpdateToastPosition(); } void GraphCanvasGraphicsView::moveEvent(QMoveEvent* event) { QGraphicsView::moveEvent(event); - UpdateToastPosition(); + m_notificationsView->UpdateToastPosition(); } void GraphCanvasGraphicsView::scrollContentsBy(int dx, int dy) @@ -1266,25 +1228,14 @@ namespace GraphCanvas { QGraphicsView::showEvent(showEvent); - if (m_activeNotification.IsValid()) - { - DisplayQueuedNotification(); - } + m_notificationsView->OnShow(); } void GraphCanvasGraphicsView::hideEvent(QHideEvent* hideEvent) { QGraphicsView::hideEvent(hideEvent); - if (m_activeNotification.IsValid()) - { - auto notificationIter = m_notifications.find(m_activeNotification); - - if (notificationIter != m_notifications.end()) - { - notificationIter->second->hide(); - } - } + m_notificationsView->OnHide(); } void GraphCanvasGraphicsView::OnSettingsChanged() @@ -1378,25 +1329,6 @@ namespace GraphCanvas BookmarkManagerRequestBus::Event(sceneId, &BookmarkManagerRequests::ActivateShortcut, bookmarkShortcut); } - void GraphCanvasGraphicsView::UpdateToastPosition() - { - if (m_activeNotification.IsValid()) - { - auto notificationIter = m_notifications.find(m_activeNotification); - - if (notificationIter != m_notifications.end()) - { - // Want this to be roughly in the top right corner of the graphics view. - QPoint globalPoint = mapToGlobal(QPoint(width() - 10, 10)); - - // Anchor point will be top right - QPointF anchorPoint = QPointF(1, 0); - - notificationIter->second->UpdatePosition(globalPoint, anchorPoint); - } - } - } - void GraphCanvasGraphicsView::CenterOnSceneMembers(const AZStd::vector& memberIds) { QRectF boundingRect; @@ -1642,48 +1574,4 @@ namespace GraphCanvas AZ::TickBus::Handler::BusDisconnect(); } } - - void GraphCanvasGraphicsView::OnNotificationHidden() - { - m_activeNotification.SetInvalid(); - - if (!m_queuedNotifications.empty()) - { - DisplayQueuedNotification(); - } - } - - void GraphCanvasGraphicsView::DisplayQueuedNotification() - { - if (m_queuedNotifications.empty() && isVisible()) - { - return; - } - - ToastId toastId = m_queuedNotifications.front(); - m_queuedNotifications.erase(m_queuedNotifications.begin()); - - auto notificationIter = m_notifications.find(toastId); - - if (notificationIter != m_notifications.end()) - { - m_activeNotification = toastId; - - // Want this to be roughly in the top right corner of the graphics view. - QPoint globalPoint = mapToGlobal(QPoint(width() - 10, 10)); - - // Anchor point will be top right - QPointF anchorPoint = QPointF(1, 0); - - notificationIter->second->ShowToastAtPoint(globalPoint, anchorPoint); - - QObject::connect(notificationIter->second, &ToastNotification::ToastNotificationHidden, this, &GraphCanvasGraphicsView::OnNotificationHidden); - } - - // If we didn't actually show something, recurse to avoid things getting stuck in the queue. - if (!m_activeNotification.IsValid()) - { - DisplayQueuedNotification(); - } - } } diff --git a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/GraphCanvasGraphicsView/GraphCanvasGraphicsView.h b/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/GraphCanvasGraphicsView/GraphCanvasGraphicsView.h index 70b3e16166..f7b34b598c 100644 --- a/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/GraphCanvasGraphicsView/GraphCanvasGraphicsView.h +++ b/Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/GraphCanvasGraphicsView/GraphCanvasGraphicsView.h @@ -27,11 +27,11 @@ AZ_POP_DISABLE_WARNING #include #include +#include #include #include #include -#include namespace GraphCanvas { @@ -123,11 +123,11 @@ namespace GraphCanvas void RefreshView() override; - void HideToastNotification(const ToastId& toastId) override; + void HideToastNotification(const AzToolsFramework::ToastId& toastId) override; - ToastId ShowToastNotification(const ToastConfiguration& toastConfiguration) override; - ToastId ShowToastAtCursor(const ToastConfiguration& toastConfiguration) override; - ToastId ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const ToastConfiguration& toastConfiguration) override; + AzToolsFramework::ToastId ShowToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration) override; + AzToolsFramework::ToastId ShowToastAtCursor(const AzQtComponents::ToastConfiguration& toastConfiguration) override; + AzToolsFramework::ToastId ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const AzQtComponents::ToastConfiguration& toastConfiguration) override; bool IsShowing() const override; //// @@ -186,8 +186,6 @@ namespace GraphCanvas private: - void UpdateToastPosition(); - void CenterOnSceneMembers(const AZStd::vector& memberIds); void ConnectBoundsSignals(); @@ -206,9 +204,6 @@ namespace GraphCanvas void ManageTickState(); - void OnNotificationHidden(); - void DisplayQueuedNotification(); - GraphCanvasGraphicsView(const GraphCanvasGraphicsView&) = delete; ViewId m_viewId; @@ -242,13 +237,7 @@ namespace GraphCanvas AZStd::unique_ptr m_queuedFocus; - // These will display sequentially in a reserved part of the UI - ToastId m_activeNotification; - AZStd::vector< ToastId > m_queuedNotifications; - - // There could be more then the queued list in terms of general notifications - // As some systems might want to re-use the systems for their own needs. - AZStd::unordered_map< ToastId, ToastNotification* > m_notifications; + AZStd::unique_ptr m_notificationsView; bool m_isEditing; diff --git a/Gems/GraphCanvas/Code/graphcanvas_staticlib_files.cmake b/Gems/GraphCanvas/Code/graphcanvas_staticlib_files.cmake index bedc6329d5..b62ff303a6 100644 --- a/Gems/GraphCanvas/Code/graphcanvas_staticlib_files.cmake +++ b/Gems/GraphCanvas/Code/graphcanvas_staticlib_files.cmake @@ -30,7 +30,6 @@ set(FILES StaticLib/GraphCanvas/Components/PersistentIdBus.h StaticLib/GraphCanvas/Components/SceneBus.h StaticLib/GraphCanvas/Components/StyleBus.h - StaticLib/GraphCanvas/Components/ToastBus.h StaticLib/GraphCanvas/Components/ViewBus.h StaticLib/GraphCanvas/Components/VisualBus.h StaticLib/GraphCanvas/GraphicsItems/AnimatedPulse.cpp @@ -222,9 +221,6 @@ set(FILES StaticLib/GraphCanvas/Widgets/StyledItemDelegates/GenericComboBoxDelegate.h StaticLib/GraphCanvas/Widgets/StyledItemDelegates/IconDecoratedNameDelegate.cpp StaticLib/GraphCanvas/Widgets/StyledItemDelegates/IconDecoratedNameDelegate.h - StaticLib/GraphCanvas/Widgets/ToastNotification/ToastNotification.cpp - StaticLib/GraphCanvas/Widgets/ToastNotification/ToastNotification.h - StaticLib/GraphCanvas/Widgets/ToastNotification/ToastNotification.ui StaticLib/GraphCanvas/Utils/ColorUtils.h StaticLib/GraphCanvas/Utils/ConversionUtils.h StaticLib/GraphCanvas/Utils/GraphUtils.cpp diff --git a/Gems/ImGui/Code/Include/LYImGuiUtils/HistogramContainer.h b/Gems/ImGui/Code/Include/LYImGuiUtils/HistogramContainer.h index 6129c3ec08..e31d5743c3 100644 --- a/Gems/ImGui/Code/Include/LYImGuiUtils/HistogramContainer.h +++ b/Gems/ImGui/Code/Include/LYImGuiUtils/HistogramContainer.h @@ -85,6 +85,10 @@ namespace ImGui //! Calculate the min and maximum values for the present samples. void CalcMinMaxValues(float& outMin, float& outMax); + //! Set/get color used by either the lines in case ViewType is Lines or bars in case or Histogram. + void SetBarLineColor(const ImColor& color) { m_barLineColor = color; } + ImColor GetBarLineColor() const { return m_barLineColor; } + private: // Set the Max Size and clear the container void SetMaxSize(int size); @@ -99,6 +103,7 @@ namespace ImGui bool m_dispalyOverlays; ScaleMode m_scaleMode; //! Determines if the vertical range of the histogram will be manually specified, auto-expanded or automatically scaled based on the samples. float m_autoScaleSpeed = 0.05f; //! Indicates how fast the min max values and the visible vertical range are adapting to new samples. + ImColor m_barLineColor = ImColor(66, 166, 178); //! Color used by either the lines in case ViewType is Lines or bars in case or Histogram. bool m_collapsed; bool m_drawMostRecentValueText; }; diff --git a/Gems/ImGui/Code/Source/LYImGuiUtils/HistogramContainer.cpp b/Gems/ImGui/Code/Source/LYImGuiUtils/HistogramContainer.cpp index 1915cc3721..51bcc29b99 100644 --- a/Gems/ImGui/Code/Source/LYImGuiUtils/HistogramContainer.cpp +++ b/Gems/ImGui/Code/Source/LYImGuiUtils/HistogramContainer.cpp @@ -147,6 +147,8 @@ namespace ImGui float imGuiHistoWidgetHeight = m_collapsed ? histogramHeight : (histogramHeight - 15); if (GetSize() > 0) { + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, m_barLineColor.Value); + switch (m_viewType) { default: @@ -160,6 +162,8 @@ namespace ImGui ImGui::PlotLines(AZStd::string::format("##%s_lines", m_histogramName.c_str()).c_str(), ImGui::LYImGuiUtils::s_histogramContainerGetter, this, GetSize(), 0, m_histogramName.c_str(), m_minScale, m_maxScale, ImVec2(histogramWidth - 10, imGuiHistoWidgetHeight)); break; } + + ImGui::PopStyleColor(); } diff --git a/Gems/LmbrCentral/Code/Source/Shape/AxisAlignedBoxShapeComponent.h b/Gems/LmbrCentral/Code/Source/Shape/AxisAlignedBoxShapeComponent.h index 094f9591d1..e180e1d536 100644 --- a/Gems/LmbrCentral/Code/Source/Shape/AxisAlignedBoxShapeComponent.h +++ b/Gems/LmbrCentral/Code/Source/Shape/AxisAlignedBoxShapeComponent.h @@ -15,12 +15,6 @@ namespace LmbrCentral { - /// Type ID for the AxisAlignedBoxShapeComponent - static const AZ::Uuid AxisAlignedBoxShapeComponentTypeId = "{641D817E-1BC6-406A-BBB2-218541808E45}"; - - /// Type ID for the EditorAxisAlignedBoxShapeComponent - static const AZ::Uuid EditorAxisAlignedBoxShapeComponentTypeId = "{8C027DF6-E157-4159-9BF8-F1B925466F1F}"; - /// Provide a Component interface for AxisAlignedBoxShape functionality. class AxisAlignedBoxShapeComponent : public AZ::Component diff --git a/Gems/LmbrCentral/Code/include/LmbrCentral/Shape/BoxShapeComponentBus.h b/Gems/LmbrCentral/Code/include/LmbrCentral/Shape/BoxShapeComponentBus.h index 488e8b5617..2e13334a76 100644 --- a/Gems/LmbrCentral/Code/include/LmbrCentral/Shape/BoxShapeComponentBus.h +++ b/Gems/LmbrCentral/Code/include/LmbrCentral/Shape/BoxShapeComponentBus.h @@ -24,6 +24,12 @@ namespace LmbrCentral /// Type ID for the BoxShapeConfig static const AZ::Uuid BoxShapeConfigTypeId = "{F034FBA2-AC2F-4E66-8152-14DFB90D6283}"; + /// Type ID for the AxisAlignedBoxShapeComponent + static const AZ::Uuid AxisAlignedBoxShapeComponentTypeId = "{641D817E-1BC6-406A-BBB2-218541808E45}"; + + /// Type ID for the EditorAxisAlignedBoxShapeComponent + static const AZ::Uuid EditorAxisAlignedBoxShapeComponentTypeId = "{8C027DF6-E157-4159-9BF8-F1B925466F1F}"; + /// Configuration data for BoxShapeComponent class BoxShapeConfig : public ShapeComponentConfig diff --git a/Gems/Multiplayer/Code/CMakeLists.txt b/Gems/Multiplayer/Code/CMakeLists.txt index b0dcb971b0..559fa23553 100644 --- a/Gems/Multiplayer/Code/CMakeLists.txt +++ b/Gems/Multiplayer/Code/CMakeLists.txt @@ -177,6 +177,11 @@ if (PAL_TRAIT_BUILD_TESTS_SUPPORTED) PRIVATE AZ::AzTest Gem::Multiplayer.Static + AUTOGEN_RULES + *.AutoComponent.xml,AutoComponent_Header.jinja,$path/$fileprefix.AutoComponent.h + *.AutoComponent.xml,AutoComponent_Source.jinja,$path/$fileprefix.AutoComponent.cpp + *.AutoComponent.xml,AutoComponentTypes_Header.jinja,$path/AutoComponentTypes.h + *.AutoComponent.xml,AutoComponentTypes_Source.jinja,$path/AutoComponentTypes.cpp ) ly_add_googletest( NAME Gem::Multiplayer.Tests diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h index bcbea6542f..678ec2d6fd 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h @@ -57,6 +57,14 @@ namespace Multiplayer const AzNetworking::PacketEncodingBuffer& correction ) override; + //! Forcibly enables ProcessInput to execute on the entity. + //! Note that this function is quite dangerous and should normally never be used + void ForceEnableAutonomousUpdate(); + + //! Forcibly disables ProcessInput from executing on the entity. + //! Note that this function is quite dangerous and should normally never be used + void ForceDisableAutonomousUpdate(); + //! Return true if we're currently migrating from one host to another. //! @return boolean true if we're currently migrating from one host to another bool IsMigrating() const; @@ -71,6 +79,8 @@ namespace Multiplayer void UpdateAutonomous(AZ::TimeMs deltaTimeMs); void UpdateBankedTime(AZ::TimeMs deltaTimeMs); + bool SerializeEntityCorrection(AzNetworking::ISerializer& serializer); + using StateHistoryItem = AZStd::unique_ptr; AZStd::map m_predictiveStateHistory; diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h index 265137a078..577e29e34f 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h @@ -32,7 +32,7 @@ namespace Multiplayer using EntityStopEvent = AZ::Event; using EntityDirtiedEvent = AZ::Event<>; using EntitySyncRewindEvent = AZ::Event<>; - using EntityServerMigrationEvent = AZ::Event; + using EntityServerMigrationEvent = AZ::Event; using EntityPreRenderEvent = AZ::Event; using EntityCorrectionEvent = AZ::Event<>; @@ -113,7 +113,7 @@ namespace Multiplayer void MarkDirty(); void NotifyLocalChanges(); void NotifySyncRewindState(); - void NotifyServerMigration(const HostId& hostId, AzNetworking::ConnectionId connectionId); + void NotifyServerMigration(const HostId& remoteHostId); void NotifyPreRender(float deltaTime); void NotifyCorrection(); diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyChildComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyChildComponent.h index 544fb3d6cb..af1878dd49 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyChildComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyChildComponent.h @@ -58,16 +58,13 @@ namespace Multiplayer void BindNetworkHierarchyLeaveEventHandler(NetworkHierarchyLeaveEvent::Handler& handler) override; //! @} - protected: + private: //! Used by @NetworkHierarchyRootComponent - void SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot); + void SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot); - private: AZ::ChildChangedEvent::Handler m_childChangedHandler; - AZ::ParentChangedEvent::Handler m_parentChangedHandler; void OnChildChanged(AZ::ChildChangeType type, AZ::EntityId child); - void OnParentChanged(AZ::EntityId oldParent, AZ::EntityId parent); //! Points to the top level root. AZ::Entity* m_rootEntity = nullptr; @@ -82,5 +79,8 @@ namespace Multiplayer bool m_isHierarchyEnabled = true; void NotifyChildrenHierarchyDisbanded(); + + AzNetworking::ConnectionId m_previousOwningConnectionId = AzNetworking::InvalidConnectionId; + void SetOwningConnectionId(AzNetworking::ConnectionId connectionId) override; }; } diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h index 674dc6114d..9b78cc43e6 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h @@ -29,6 +29,7 @@ namespace Multiplayer , public NetworkHierarchyRequestBus::Handler { friend class NetworkHierarchyChildComponent; + friend class NetworkHierarchyRootComponentController; public: AZ_MULTIPLAYER_COMPONENT(Multiplayer::NetworkHierarchyRootComponent, s_networkHierarchyRootComponentConcreteUuid, Multiplayer::NetworkHierarchyRootComponentBase); @@ -57,10 +58,11 @@ namespace Multiplayer void BindNetworkHierarchyLeaveEventHandler(NetworkHierarchyLeaveEvent::Handler& handler) override; //! @} - protected: - void SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot); + bool SerializeEntityCorrection(AzNetworking::ISerializer& serializer); private: + void SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot); + AZ::ChildChangedEvent::Handler m_childChangedHandler; AZ::ParentChangedEvent::Handler m_parentChangedHandler; @@ -77,26 +79,39 @@ namespace Multiplayer //! Rebuilds hierarchy starting from this root component's entity. void RebuildHierarchy(); - - //! @param underEntity Walk the child entities that belong to @underEntity and consider adding them to the hierarchy - //! @param currentEntityCount The total number of entities in the hierarchy prior to calling this method, - //! used to avoid adding too many entities to the hierarchy while walking recursively the relevant entities. - //! @currentEntityCount will be modified to reflect the total entity count upon completion of this method. - //! @returns false if an attempt was made to go beyond the maximum supported hierarchy size, true otherwise - bool RecursiveAttachHierarchicalEntities(AZ::EntityId underEntity, uint32_t& currentEntityCount); - - //! @param entity Add the child entity and any of its relevant children to the hierarchy - //! @param currentEntityCount The total number of entities in the hierarchy prior to calling this method, - //! used to avoid adding too many entities to the hierarchy while walking recursively the relevant entities. - //! @currentEntityCount will be modified to reflect the total entity count upon completion of this method. - //! @returns false if an attempt was made to go beyond the maximum supported hierarchy size, true otherwise - bool RecursiveAttachHierarchicalChild(AZ::EntityId entity, uint32_t& currentEntityCount); - - void SetRootForEntity(AZ::Entity* root, const AZ::Entity* childEntity); - + + //! @param underEntity Walk the child entities that belong to @underEntity and consider adding them to the hierarchy. + //! Builds the hierarchy using breadth-first iterative method. + void InternalBuildHierarchyList(AZ::Entity* underEntity); + + void SetRootForEntity(AZ::Entity* previousKnownRoot, AZ::Entity* newRoot, const AZ::Entity* childEntity); + //! Set to false when deactivating or otherwise not to be included in hierarchy considerations. bool m_isHierarchyEnabled = true; + AzNetworking::ConnectionId m_previousOwningConnectionId = AzNetworking::InvalidConnectionId; + void SetOwningConnectionId(AzNetworking::ConnectionId connectionId) override; + friend class HierarchyBenchmarkBase; }; + + + //! NetworkHierarchyRootComponentController + //! This is the network controller for NetworkHierarchyRootComponent. + //! Class provides the ability to process input for hierarchies. + class NetworkHierarchyRootComponentController final + : public NetworkHierarchyRootComponentControllerBase + { + public: + NetworkHierarchyRootComponentController(NetworkHierarchyRootComponent& parent); + + // NetworkHierarchyRootComponentControllerBase + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + //! MultiplayerController interface + Multiplayer::MultiplayerController::InputPriorityOrder GetInputOrder() const override; + void CreateInput(Multiplayer::NetworkInput& input, float deltaTime) override; + void ProcessInput(Multiplayer::NetworkInput& input, float deltaTime) override; + }; } diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h index 19289e1e42..7245dbde9b 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h @@ -45,7 +45,7 @@ namespace Multiplayer using ClientMigrationStartEvent = AZ::Event; using ClientMigrationEndEvent = AZ::Event<>; using ClientDisconnectedEvent = AZ::Event<>; - using NotifyClientMigrationEvent = AZ::Event; + using NotifyClientMigrationEvent = AZ::Event; using NotifyEntityMigrationEvent = AZ::Event; using ConnectionAcquiredEvent = AZ::Event; using ServerAcceptanceReceivedEvent = AZ::Event<>; @@ -136,10 +136,12 @@ namespace Multiplayer virtual void AddSessionShutdownHandler(SessionShutdownEvent::Handler& handler) = 0; //! Signals a NotifyClientMigrationEvent with the provided parameters. - //! @param hostId the host id of the host the client is migrating to - //! @param userIdentifier the user identifier the client will provide the new host to validate identity - //! @param lastClientInputId the last processed clientInputId by the current host - virtual void SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId) = 0; + //! @param connectionId the connection id of the client that is migrating + //! @param hostId the host id of the host the client is migrating to + //! @param userIdentifier the user identifier the client will provide the new host to validate identity + //! @param lastClientInputId the last processed clientInputId by the current host + //! @param controlledEntityId the entityId of the clients autonomous entity + virtual void SendNotifyClientMigrationEvent(AzNetworking::ConnectionId connectionId, const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) = 0; //! Signals a NotifyEntityMigrationEvent with the provided parameters. //! @param entityHandle the network entity handle of the entity being migrated @@ -181,6 +183,18 @@ namespace Multiplayer //! @return pointer to the filtered entity manager, or nullptr if not set virtual IFilterEntityManager* GetFilterEntityManager() = 0; + //! Registers a temp userId to allow a host to look up a players controlled entity in the event of a rejoin or migration event. + //! @param temporaryUserIdentifier the temporary user identifier used to identify a player across hosts + //! @param controlledEntityId the controlled entityId of the players autonomous entity + virtual void RegisterPlayerIdentifierForRejoin(uint64_t temporaryUserIdentifier, NetEntityId controlledEntityId) = 0; + + //! Completes a client migration event by informing the appropriate client to migrate between hosts. + //! @param temporaryUserIdentifier the temporary user identifier used to identify a player across hosts + //! @param connectionId the connection id of the player being migrated + //! @param publicHostId the public address of the new host the client should connect to + //! @param migratedClientInputId the last clientInputId processed prior to migration + virtual void CompleteClientMigration(uint64_t temporaryUserIdentifier, AzNetworking::ConnectionId connectionId, const HostId& publicHostId, ClientInputId migratedClientInputId) = 0; + //! Enables or disables automatic instantiation of netbound entities. //! This setting is controlled by the networking layer and should not be touched //! If enabled, netbound entities will instantiate as spawnables are loaded into the game world, generally true for the server diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h index 96035083d8..58a0ae63fa 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h @@ -29,7 +29,7 @@ namespace Multiplayer using HostId = AzNetworking::IpAddress; static const HostId InvalidHostId = HostId(); - AZ_TYPE_SAFE_INTEGRAL(NetEntityId, uint32_t); + AZ_TYPE_SAFE_INTEGRAL(NetEntityId, uint64_t); static constexpr NetEntityId InvalidNetEntityId = static_cast(-1); AZ_TYPE_SAFE_INTEGRAL(NetComponentId, uint16_t); @@ -68,6 +68,7 @@ namespace Multiplayer Server, // A simulated proxy on a server Authority // An authoritative proxy on a server (full authority) }; + const char* GetEnumString(NetEntityRole value); enum class ComponentSerializationType : uint8_t { @@ -113,6 +114,24 @@ namespace Multiplayer bool Serialize(AzNetworking::ISerializer& serializer); }; + inline const char* GetEnumString(NetEntityRole value) + { + switch (value) + { + case NetEntityRole::InvalidRole: + return "InvalidRole"; + case NetEntityRole::Client: + return "Client"; + case NetEntityRole::Autonomous: + return "Autonomous"; + case NetEntityRole::Server: + return "Server"; + case NetEntityRole::Authority: + return "Authority"; + } + return "Unknown"; + } + inline PrefabEntityId::PrefabEntityId(AZ::Name name, uint32_t entityOffset) : m_prefabName(name) , m_entityOffset(entityOffset) diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h index 2e1f83ae38..10346ad777 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h @@ -57,7 +57,6 @@ namespace Multiplayer EntityReplicationManager(AzNetworking::IConnection& connection, AzNetworking::IConnectionListener& connectionListener, Mode mode); ~EntityReplicationManager() = default; - void SetRemoteHostId(const HostId& hostId); const HostId& GetRemoteHostId() const; void ActivatePendingEntities(); diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h index 90f622a8ae..139db2a949 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h @@ -43,8 +43,7 @@ namespace Multiplayer //! Constructor for an entity delete message. //! @param entityId the networkId of the entity being deleted //! @param isMigrated whether or not the entity is being migrated or deleted - //! @param takeOwnership true if the remote replicator should take ownership of the entity - explicit NetworkEntityUpdateMessage(NetEntityId entityId, bool isMigrated, bool takeOwnership); + explicit NetworkEntityUpdateMessage(NetEntityId entityId, bool isMigrated); NetworkEntityUpdateMessage& operator =(NetworkEntityUpdateMessage&& rhs); NetworkEntityUpdateMessage& operator =(const NetworkEntityUpdateMessage& rhs); @@ -71,10 +70,6 @@ namespace Multiplayer //! @return whether or not the entity was migrated bool GetWasMigrated() const; - //! Gets the current value of TakeOwnership. - //! @return the current value of TakeOwnership - bool GetTakeOwnership() const; - //! Gets the current value of HasValidPrefabId. //! @return the current value of HasValidPrefabId bool GetHasValidPrefabId() const; @@ -110,7 +105,6 @@ namespace Multiplayer NetEntityId m_entityId = InvalidNetEntityId; bool m_isDelete = false; bool m_wasMigrated = false; - bool m_takeOwnership = false; bool m_hasValidPrefabId = false; PrefabEntityId m_prefabEntityId; diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputArray.h similarity index 100% rename from Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.h rename to Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputArray.h diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputChild.h similarity index 80% rename from Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h rename to Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputChild.h index 98778cce17..417d9d65f7 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputChild.h @@ -12,10 +12,7 @@ namespace Multiplayer { - //! Max number of entities that can be children of our netbound player entity. - static constexpr uint32_t MaxEntityHierarchyChildren = 16; - - //! Used by the EntityHierarchyComponent. This component allows the gameplay programmer to specify inputs for dependent entities. + //! Used by the NetworkHierarchyRootComponent. This component allows the gameplay programmer to specify inputs for dependent entities. //! Since it is possible to for the Client/Server to disagree about the state of related entities, //! this network input encodes the entity that is associated with it. class NetworkInputChild @@ -37,4 +34,6 @@ namespace Multiplayer ConstNetworkEntityHandle m_owner; NetworkInput m_networkInput; }; + + using NetworkInputChildList = AZStd::vector; } diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputHistory.h similarity index 100% rename from Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.h rename to Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputHistory.h diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputMigrationVector.h similarity index 100% rename from Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.h rename to Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputMigrationVector.h diff --git a/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml index 1a7496a77d..31751469de 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml @@ -9,12 +9,13 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + - - - + + + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml index 091043f034..a5bf169fb4 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml @@ -9,6 +9,7 @@ + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml index 0f33e1f642..b789506d0f 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml @@ -4,11 +4,15 @@ Name="NetworkHierarchyRootComponent" Namespace="Multiplayer" OverrideComponent="true" - OverrideController="false" + OverrideController="true" OverrideInclude="Multiplayer/Components/NetworkHierarchyRootComponent.h" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + + - + + + diff --git a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp index 747f53d37b..96d6a5e31e 100644 --- a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace Multiplayer { @@ -211,7 +212,7 @@ namespace Multiplayer m_lastCorrectionSentTimeMs = currentTimeMs; AzNetworking::HashSerializer hashSerializer; - GetNetBindComponent()->SerializeEntityCorrection(hashSerializer); + SerializeEntityCorrection(hashSerializer); const AZ::HashValue32 localAuthorityHash = hashSerializer.GetHash(); @@ -233,7 +234,7 @@ namespace Multiplayer // only deserialize if we have data (for client/server profile/debug mismatches) if (correction.GetSize() > 0) { - GetNetBindComponent()->SerializeEntityCorrection(serializer); + SerializeEntityCorrection(serializer); } correction.Resize(serializer.GetSize()); @@ -313,7 +314,7 @@ namespace Multiplayer // Apply the correction AzNetworking::TrackChangedSerializer serializer(correction.GetBuffer(), static_cast(correction.GetSize())); - GetNetBindComponent()->SerializeEntityCorrection(serializer); + SerializeEntityCorrection(serializer); GetNetBindComponent()->NotifyCorrection(); #ifndef AZ_RELEASE_BUILD @@ -325,7 +326,7 @@ namespace Multiplayer { // Read out state values AzNetworking::StringifySerializer serverValues; - GetNetBindComponent()->SerializeEntityCorrection(serverValues); + SerializeEntityCorrection(serverValues); PrintCorrectionDifferences(*iter->second, serverValues); } else @@ -353,6 +354,16 @@ namespace Multiplayer } } + void LocalPredictionPlayerInputComponentController::ForceEnableAutonomousUpdate() + { + m_autonomousUpdateEvent.Enqueue(AZ::TimeMs{ 1 }, true); + } + + void LocalPredictionPlayerInputComponentController::ForceDisableAutonomousUpdate() + { + m_autonomousUpdateEvent.RemoveFromQueue(); + } + bool LocalPredictionPlayerInputComponentController::IsMigrating() const { return m_lastMigratedInputId != ClientInputId{ 0 }; @@ -452,7 +463,7 @@ namespace Multiplayer // Generate a hash based on the current client predicted states AzNetworking::HashSerializer hashSerializer; - GetNetBindComponent()->SerializeEntityCorrection(hashSerializer); + SerializeEntityCorrection(hashSerializer); // Save this input and discard move history outside our client rewind window m_inputHistory.PushBack(input); @@ -480,7 +491,7 @@ namespace Multiplayer { m_predictiveStateHistory.erase(m_predictiveStateHistory.begin()); } - GetNetBindComponent()->SerializeEntityCorrection(*inputHistory); + SerializeEntityCorrection(*inputHistory); m_predictiveStateHistory.emplace(m_clientInputId, AZStd::move(inputHistory)); } #endif @@ -493,6 +504,18 @@ namespace Multiplayer } } + bool LocalPredictionPlayerInputComponentController::SerializeEntityCorrection(AzNetworking::ISerializer& serializer) + { + bool result = GetNetBindComponent()->SerializeEntityCorrection(serializer); + + NetworkHierarchyRootComponent* hierarchyComponent = GetParent().GetNetworkHierarchyRootComponent(); + if (result && hierarchyComponent) + { + result = hierarchyComponent->SerializeEntityCorrection(serializer); + } + return result; + } + void LocalPredictionPlayerInputComponentController::UpdateBankedTime(AZ::TimeMs deltaTimeMs) { const double deltaTime = static_cast(deltaTimeMs) / 1000.0; diff --git a/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp index fbf83b6e6c..cc71000d33 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp @@ -394,9 +394,9 @@ namespace Multiplayer m_syncRewindEvent.Signal(); } - void NetBindComponent::NotifyServerMigration(const HostId& hostId, AzNetworking::ConnectionId connectionId) + void NetBindComponent::NotifyServerMigration(const HostId& remoteHostId) { - m_entityServerMigrationEvent.Signal(m_netEntityHandle, hostId, connectionId); + m_entityServerMigrationEvent.Signal(m_netEntityHandle, remoteHostId); } void NetBindComponent::NotifyPreRender(float deltaTime) diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp index 3124eccaa8..59782b1f4d 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp @@ -55,7 +55,6 @@ namespace Multiplayer NetworkHierarchyChildComponent::NetworkHierarchyChildComponent() : m_childChangedHandler([this](AZ::ChildChangeType type, AZ::EntityId child) { OnChildChanged(type, child); }) - , m_parentChangedHandler([this](AZ::EntityId oldParent, AZ::EntityId parent) { OnParentChanged(oldParent, parent); }) , m_hierarchyRootNetIdChanged([this](NetEntityId rootNetId) {OnHierarchyRootNetIdChanged(rootNetId); }) { @@ -75,7 +74,6 @@ namespace Multiplayer if (AzFramework::TransformComponent* transformComponent = GetEntity()->FindComponent()) { transformComponent->BindChildChangedEventHandler(m_childChangedHandler); - transformComponent->BindParentChangedEventHandler(m_parentChangedHandler); } } @@ -131,45 +129,52 @@ namespace Multiplayer handler.Connect(m_networkHierarchyLeaveEvent); } - void NetworkHierarchyChildComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot) + void NetworkHierarchyChildComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot) { - m_rootEntity = hierarchyRoot; - if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) + if (newHierarchyRoot) { - NetworkHierarchyChildComponentController* controller = static_cast(GetController()); - if (m_rootEntity) + if (m_rootEntity != newHierarchyRoot) { - const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId()); - controller->SetHierarchyRoot(netRootId); + m_rootEntity = newHierarchyRoot; + if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) + { + NetworkHierarchyChildComponentController* controller = static_cast(GetController()); + const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId()); + controller->SetHierarchyRoot(netRootId); + } + + GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent()->GetOwningConnectionId()); m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId()); } - else + } + else if ((previousHierarchyRoot && m_rootEntity == previousHierarchyRoot) || !previousHierarchyRoot) + { + m_rootEntity = nullptr; + + if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) { + NetworkHierarchyChildComponentController* controller = static_cast(GetController()); controller->SetHierarchyRoot(InvalidNetEntityId); - - m_networkHierarchyLeaveEvent.Signal(); } - } - if (m_rootEntity == nullptr) - { + GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); + m_networkHierarchyLeaveEvent.Signal(); + NotifyChildrenHierarchyDisbanded(); } } - void NetworkHierarchyChildComponent::OnChildChanged([[maybe_unused]] AZ::ChildChangeType type, [[maybe_unused]] AZ::EntityId child) + void NetworkHierarchyChildComponent::SetOwningConnectionId(AzNetworking::ConnectionId connectionId) { - if (m_rootEntity) + NetworkHierarchyChildComponentBase::SetOwningConnectionId(connectionId); + if (IsHierarchicalChild() == false) { - if (NetworkHierarchyRootComponent* root = m_rootEntity->FindComponent()) - { - root->RebuildHierarchy(); - } + m_previousOwningConnectionId = connectionId; } } - void NetworkHierarchyChildComponent::OnParentChanged([[maybe_unused]] AZ::EntityId oldParent, [[maybe_unused]] AZ::EntityId parent) + void NetworkHierarchyChildComponent::OnChildChanged([[maybe_unused]] AZ::ChildChangeType type, [[maybe_unused]] AZ::EntityId child) { if (m_rootEntity) { @@ -189,32 +194,38 @@ namespace Multiplayer if (m_rootEntity != newRoot) { m_rootEntity = newRoot; + + m_previousOwningConnectionId = GetNetBindComponent()->GetOwningConnectionId(); + GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent()->GetOwningConnectionId()); + m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId()); } } else { + GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); m_isHierarchyEnabled = false; m_rootEntity = nullptr; - m_networkHierarchyLeaveEvent.Signal(); } } void NetworkHierarchyChildComponent::NotifyChildrenHierarchyDisbanded() { + AZ::ComponentApplicationRequests* componentApplication = AZ::Interface::Get(); + AZStd::vector allChildren; AZ::TransformBus::EventResult(allChildren, GetEntityId(), &AZ::TransformBus::Events::GetChildren); for (const AZ::EntityId& childEntityId : allChildren) { - if (const AZ::Entity* childEntity = AZ::Interface::Get()->FindEntity(childEntityId)) + if (const AZ::Entity* childEntity = componentApplication->FindEntity(childEntityId)) { if (auto* hierarchyChildComponent = childEntity->FindComponent()) { - hierarchyChildComponent->SetTopLevelHierarchyRootEntity(nullptr); + hierarchyChildComponent->SetTopLevelHierarchyRootEntity(nullptr, nullptr); } else if (auto* hierarchyRootComponent = childEntity->FindComponent()) { - hierarchyRootComponent->SetTopLevelHierarchyRootEntity(nullptr); + hierarchyRootComponent->SetTopLevelHierarchyRootEntity(nullptr, nullptr); } } } diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp index 4a5e9ce8d2..76f4bddb1a 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp @@ -20,6 +20,8 @@ AZ_CVAR(uint32_t, bg_hierarchyEntityMaxLimit, 16, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum allowed size of network entity hierarchies, including top level entity."); +static constexpr int CommonHierarchyEntityMaxLimit = 16; // Should match @bg_hierarchyEntityMaxLimit + namespace Multiplayer { void NetworkHierarchyRootComponent::Reflect(AZ::ReflectContext* context) @@ -105,7 +107,7 @@ namespace Multiplayer { if (const AZ::Entity* childEntity = AZ::Interface::Get()->FindEntity(childEntityId)) { - SetRootForEntity(nullptr, childEntity); + SetRootForEntity(GetEntity(), nullptr, childEntity); } } } @@ -173,6 +175,29 @@ namespace Multiplayer } } + static AZStd::tuple GetHierarchyComponents(const AZ::Entity* entity) + { + NetworkHierarchyChildComponent* childComponent = nullptr; + NetworkHierarchyRootComponent* rootComponent = nullptr; + + for (AZ::Component* component : entity->GetComponents()) + { + if (component->GetUnderlyingComponentType() == NetworkHierarchyChildComponent::TYPEINFO_Uuid()) + { + childComponent = static_cast(component); + break; + } + + if (component->GetUnderlyingComponentType() == NetworkHierarchyRootComponent::TYPEINFO_Uuid()) + { + rootComponent = static_cast(component); + break; + } + } + + return AZStd::tie(rootComponent, childComponent); + } + void NetworkHierarchyRootComponent::OnParentChanged([[maybe_unused]] AZ::EntityId oldParent, AZ::EntityId newParent) { // If the parent is part of a hierarchy, it will detect this entity as a new child and rebuild hierarchy. @@ -181,10 +206,10 @@ namespace Multiplayer if (AZ::Entity* parentEntity = AZ::Interface::Get()->FindEntity(newParent)) { - if (parentEntity->FindComponent() == nullptr && - parentEntity->FindComponent() == nullptr) + auto [rootComponent, childComponent] = GetHierarchyComponents(parentEntity); + if (rootComponent == nullptr && childComponent == nullptr) { - RebuildHierarchy(); + SetRootForEntity(nullptr, nullptr, GetEntity()); } else { @@ -194,7 +219,7 @@ namespace Multiplayer else { // Detached from parent - RebuildHierarchy(); + SetRootForEntity(nullptr, nullptr, GetEntity()); } } @@ -203,10 +228,9 @@ namespace Multiplayer AZStd::vector previousEntities; m_hierarchicalEntities.swap(previousEntities); - m_hierarchicalEntities.push_back(GetEntity()); // Add the root. + m_hierarchicalEntities.reserve(bg_hierarchyEntityMaxLimit); - uint32_t currentEntityCount = aznumeric_cast(m_hierarchicalEntities.size()); - RecursiveAttachHierarchicalEntities(GetEntityId(), currentEntityCount); + InternalBuildHierarchyList(GetEntity()); bool hierarchyChanged = false; @@ -223,14 +247,14 @@ namespace Multiplayer { // This is a newly added entity to the network hierarchy. hierarchyChanged = true; - SetRootForEntity(GetEntity(), currentEntity); + SetRootForEntity(nullptr, GetEntity(), currentEntity); } } // These entities were removed since last rebuild. for (const AZ::Entity* previousEntity : previousEntities) { - SetRootForEntity(nullptr, previousEntity); + SetRootForEntity(GetEntity(), nullptr, previousEntity); } if (!previousEntities.empty()) @@ -244,86 +268,248 @@ namespace Multiplayer } } - void NetworkHierarchyRootComponent::SetRootForEntity(AZ::Entity* root, const AZ::Entity* childEntity) + void NetworkHierarchyRootComponent::InternalBuildHierarchyList(AZ::Entity* underEntity) { - if (auto* hierarchyChildComponent = childEntity->FindComponent()) + AZ::ComponentApplicationRequests* componentApplicationRequests = AZ::Interface::Get(); + + AZStd::deque candidates; + candidates.push_back(underEntity); + + while (!candidates.empty()) { - hierarchyChildComponent->SetTopLevelHierarchyRootEntity(root); + AZ::Entity* candidate = candidates.front(); + candidates.pop_front(); + + if (candidate) + { + auto [hierarchyRootComponent, hierarchyChildComponent] = GetHierarchyComponents(candidate); + + if ((hierarchyChildComponent && hierarchyChildComponent->IsHierarchyEnabled()) || + (hierarchyRootComponent && hierarchyRootComponent->IsHierarchyEnabled())) + { + m_hierarchicalEntities.push_back(candidate); + + if (m_hierarchicalEntities.size() >= bg_hierarchyEntityMaxLimit) + { + AZLOG_WARN("Network hierarchy size exceeded, current limit is %d, root entity was %s", + static_cast(bg_hierarchyEntityMaxLimit), + GetEntity()->GetName().c_str()); + return; + } + + const AZStd::vector allChildren = candidate->GetTransform()->GetChildren(); + for (const AZ::EntityId& newChildId : allChildren) + { + candidates.push_back(componentApplicationRequests->FindEntity(newChildId)); + } + } + } } - else if (auto* hierarchyRootComponent = childEntity->FindComponent()) + } + + void NetworkHierarchyRootComponent::SetRootForEntity(AZ::Entity* previousKnownRoot, AZ::Entity* newRoot, const AZ::Entity* childEntity) + { + auto [hierarchyRootComponent, hierarchyChildComponent] = GetHierarchyComponents(childEntity); + + if (hierarchyChildComponent) + { + hierarchyChildComponent->SetTopLevelHierarchyRootEntity(previousKnownRoot, newRoot); + } + else if (hierarchyRootComponent) { - hierarchyRootComponent->SetTopLevelHierarchyRootEntity(root); + hierarchyRootComponent->SetTopLevelHierarchyRootEntity(previousKnownRoot, newRoot); } } - bool NetworkHierarchyRootComponent::RecursiveAttachHierarchicalEntities(AZ::EntityId underEntity, uint32_t& currentEntityCount) + void NetworkHierarchyRootComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot) { - AZStd::vector allChildren; - AZ::TransformBus::EventResult(allChildren, underEntity, &AZ::TransformBus::Events::GetChildren); + if (newHierarchyRoot) + { + if (m_rootEntity != newHierarchyRoot) + { + m_rootEntity = newHierarchyRoot; + + if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) + { + NetworkHierarchyRootComponentController* controller = static_cast(GetController()); + const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId()); + controller->SetHierarchyRoot(netRootId); + } - for (const AZ::EntityId& newChildId : allChildren) + GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent()->GetOwningConnectionId()); + m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId()); + } + } + else if ((previousHierarchyRoot && m_rootEntity == previousHierarchyRoot) || !previousHierarchyRoot) { - if (!RecursiveAttachHierarchicalChild(newChildId, currentEntityCount)) + m_rootEntity = nullptr; + + if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) { - return false; + NetworkHierarchyRootComponentController* controller = static_cast(GetController()); + controller->SetHierarchyRoot(InvalidNetEntityId); } + + GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); + m_networkHierarchyLeaveEvent.Signal(); + + // We lost the parent hierarchical entity, so as a root we need to re-build our own hierarchy. + RebuildHierarchy(); + } + } + + void NetworkHierarchyRootComponent::SetOwningConnectionId(AzNetworking::ConnectionId connectionId) + { + NetworkHierarchyRootComponentBase::SetOwningConnectionId(connectionId); + if (IsHierarchicalChild() == false) + { + m_previousOwningConnectionId = connectionId; } + } + + NetworkHierarchyRootComponentController::NetworkHierarchyRootComponentController(NetworkHierarchyRootComponent& parent) + : NetworkHierarchyRootComponentControllerBase(parent) + { - return true; } - bool NetworkHierarchyRootComponent::RecursiveAttachHierarchicalChild(AZ::EntityId entity, uint32_t& currentEntityCount) + void NetworkHierarchyRootComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) { - if (currentEntityCount >= bg_hierarchyEntityMaxLimit) + + } + + void NetworkHierarchyRootComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + + } + + Multiplayer::MultiplayerController::InputPriorityOrder NetworkHierarchyRootComponentController::GetInputOrder() const + { + return Multiplayer::MultiplayerController::InputPriorityOrder::SubEntities; + } + + void NetworkHierarchyRootComponentController::CreateInput(Multiplayer::NetworkInput& input, float deltaTime) + { + NetworkHierarchyRootComponent& component = GetParent(); + if (!component.IsHierarchicalRoot()) { - AZLOG_WARN("Entity %s is trying to build a network hierarchy that is too large. bg_hierarchyEntityMaxLimit is currently set to (%u)", - GetEntity()->GetName().c_str(), static_cast(bg_hierarchyEntityMaxLimit)); - return false; + return; } - if (AZ::Entity* childEntity = AZ::Interface::Get()->FindEntity(entity)) + INetworkEntityManager* networkEntityManager = AZ::Interface::Get(); + AZ_Assert(networkEntityManager, "NetworkEntityManager must be created."); + + const AZStd::vector& entities = component.m_hierarchicalEntities; + + auto* networkInput = input.FindComponentInput(); + networkInput->m_childInputs.clear(); + networkInput->m_childInputs.reserve(entities.size()); + + for (AZ::Entity* child : entities) { - auto* hierarchyChildComponent = childEntity->FindComponent(); - auto* hierarchyRootComponent = childEntity->FindComponent(); + if (child == component.GetEntity()) + { + continue; // Avoid infinite recursion + } + + NetEntityId childNetEntitydId = networkEntityManager->GetNetEntityIdById(child->GetId()); + AZ_Assert(childNetEntitydId != InvalidNetEntityId, "Unable to find the hierarchy entity in Network Entity Manager"); + + ConstNetworkEntityHandle childEntityHandle = networkEntityManager->GetEntity(childNetEntitydId); + NetBindComponent* netComp = childEntityHandle.GetNetBindComponent(); - if ((hierarchyChildComponent && hierarchyChildComponent->IsHierarchyEnabled()) || - (hierarchyRootComponent && hierarchyRootComponent->IsHierarchyEnabled())) + AZ_Assert(netComp, "No NetBindComponent, this should be impossible"); + // Validate we still have a controller and we aren't in the middle of removing them + if (netComp->HasController()) { - m_hierarchicalEntities.push_back(childEntity); - ++currentEntityCount; + NetworkInputChild subInput; + subInput.Attach(childEntityHandle); + subInput.GetNetworkInput().SetClientInputId(input.GetClientInputId()); - if (!RecursiveAttachHierarchicalEntities(entity, currentEntityCount)) - { - return false; - } + netComp->CreateInput(subInput.GetNetworkInput(), deltaTime); + + // make sure our input sub commands have the same time as the original + subInput.GetNetworkInput().SetClientInputId(input.GetClientInputId()); + networkInput->m_childInputs.emplace_back(subInput); } } - - return true; } - void NetworkHierarchyRootComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot) + void NetworkHierarchyRootComponentController::ProcessInput(Multiplayer::NetworkInput& input, float deltaTime) { - m_rootEntity = hierarchyRoot; - - if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) + if (auto* networkInput = input.FindComponentInput()) { - NetworkHierarchyChildComponentController* controller = static_cast(GetController()); - if (hierarchyRoot) + INetworkEntityManager* networkEntityManager = AZ::Interface::Get(); + AZ_Assert(networkEntityManager, "NetworkEntityManager must be created."); + + // Build a set of Net IDs for the children + AZStd::unordered_set currentChildren; + NetworkHierarchyRootComponent& component = GetParent(); + for (AZ::Entity* child : component.m_hierarchicalEntities) { - const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(hierarchyRoot->GetId()); - controller->SetHierarchyRoot(netRootId); + if (child == component.GetEntity()) // Skip the root entity + { + continue; + } + + NetEntityId childNetEntitydId = networkEntityManager->GetNetEntityIdById(child->GetId()); + AZ_Assert(childNetEntitydId != InvalidNetEntityId, "Unable to find the hierarchy entity in Network Entity Manager"); + currentChildren.insert(childNetEntitydId); } - else + + // Process the input for the child entities + for (NetworkInputChild& subInput : networkInput->m_childInputs) { - controller->SetHierarchyRoot(InvalidNetEntityId); + const ConstNetworkEntityHandle& inputOwnerHandle = subInput.GetOwner(); + NetEntityId inputOwnerNetEntitydId = inputOwnerHandle.GetNetEntityId(); + + if (currentChildren.count(inputOwnerNetEntitydId) == 0) + { + // Skip the input for entities which are not a part of this hierarchy + continue; + } + + ConstNetworkEntityHandle localEntityHandle = networkEntityManager->GetEntity(inputOwnerNetEntitydId); + if (localEntityHandle.Exists()) + { + auto* netComp = localEntityHandle.GetNetBindComponent(); + AZ_Assert(netComp, "No NetBindComponent, this should be impossible"); + // We do not rewind entity role changes, so make sure we are the correct role prior to processing + if (netComp->HasController()) + { + subInput.GetNetworkInput().SetClientInputId(input.GetClientInputId()); + netComp->ProcessInput(subInput.GetNetworkInput(), deltaTime); + } + } } } + } + + bool NetworkHierarchyRootComponent::SerializeEntityCorrection(AzNetworking::ISerializer& serializer) + { + bool result = true; - if (m_rootEntity == nullptr) + INetworkEntityManager* networkEntityManager = AZ::Interface::Get(); + AZ_Assert(networkEntityManager, "NetworkEntityManager must be created."); + + for (AZ::Entity* child : m_hierarchicalEntities) { - // We lost the parent hierarchical entity, so as a root we need to re-build our own hierarchy. - RebuildHierarchy(); + if (child == GetEntity()) + { + // Skip the root entity + continue; + } + + NetEntityId childNetEntitydId = networkEntityManager->GetNetEntityIdById(child->GetId()); + AZ_Assert(childNetEntitydId != InvalidNetEntityId, "Unable to find the hierarchy entity in Network Entity Manager"); + + ConstNetworkEntityHandle childEntityHandle = networkEntityManager->GetEntity(childNetEntitydId); + NetBindComponent* netBindComponent = childEntityHandle.GetNetBindComponent(); + AZ_Assert(netBindComponent, "No NetBindComponent, this should be impossible"); + + result = result && netBindComponent->SerializeEntityCorrection(serializer); } + + return result; } } diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp index a9b8e03126..56eed58af7 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp @@ -23,22 +23,16 @@ namespace Multiplayer ServerToClientConnectionData::ServerToClientConnectionData ( AzNetworking::IConnection* connection, - AzNetworking::IConnectionListener& connectionListener, - NetworkEntityHandle controlledEntity + AzNetworking::IConnectionListener& connectionListener ) : m_connection(connection) , m_controlledEntityRemovedHandler([this](const ConstNetworkEntityHandle&) { OnControlledEntityRemove(); }) - , m_controlledEntityMigrationHandler([this](const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId, AzNetworking::ConnectionId connectionId) { OnControlledEntityMigration(entityHandle, remoteHostId, connectionId); }) - , m_controlledEntity(controlledEntity) + , m_controlledEntityMigrationHandler([this](const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) + { + OnControlledEntityMigration(entityHandle, remoteHostId); + }) , m_entityReplicationManager(*connection, connectionListener, EntityReplicationManager::Mode::LocalServerToRemoteClient) { - NetBindComponent* netBindComponent = m_controlledEntity.GetNetBindComponent(); - if (netBindComponent != nullptr) - { - netBindComponent->AddEntityStopEventHandler(m_controlledEntityRemovedHandler); - netBindComponent->AddEntityServerMigrationEventHandler(m_controlledEntityMigrationHandler); - } - m_entityReplicationManager.SetMaxRemoteEntitiesPendingCreationCount(sv_ClientMaxRemoteEntitiesPendingCreationCount); m_entityReplicationManager.SetEntityPendingRemovalMs(sv_ClientEntityReplicatorPendingRemovalTimeMs); } @@ -54,6 +48,20 @@ namespace Multiplayer m_controlledEntityRemovedHandler.Disconnect(); } + void ServerToClientConnectionData::SetControlledEntity(NetworkEntityHandle primaryPlayerEntity) + { + m_controlledEntityRemovedHandler.Disconnect(); + m_controlledEntityMigrationHandler.Disconnect(); + + m_controlledEntity = primaryPlayerEntity; + NetBindComponent* netBindComponent = m_controlledEntity.GetNetBindComponent(); + if (netBindComponent != nullptr) + { + netBindComponent->AddEntityStopEventHandler(m_controlledEntityRemovedHandler); + netBindComponent->AddEntityServerMigrationEventHandler(m_controlledEntityMigrationHandler); + } + } + ConnectionDataType ServerToClientConnectionData::GetConnectionDataType() const { return ConnectionDataType::ServerToClient; @@ -94,8 +102,7 @@ namespace Multiplayer void ServerToClientConnectionData::OnControlledEntityMigration ( [[maybe_unused]] const ConstNetworkEntityHandle& entityHandle, - [[maybe_unused]] const HostId& remoteHostId, - [[maybe_unused]] AzNetworking::ConnectionId connectionId + const HostId& remoteHostId ) { ClientInputId migratedClientInputId = ClientInputId{ 0 }; @@ -109,14 +116,12 @@ namespace Multiplayer } // Generate crypto-rand user identifier, send to both server and client so they can negotiate the autonomous entity to assume predictive control over after migration - const uint64_t randomUserIdentifier = AzNetworking::CryptoRand64(); + const uint64_t temporaryUserIdentifier = AzNetworking::CryptoRand64(); // Tell the new host that a client is about to (re)join - GetMultiplayer()->SendNotifyClientMigrationEvent(remoteHostId, randomUserIdentifier, migratedClientInputId); - - // Tell the client who to join - MultiplayerPackets::ClientMigration clientMigration(remoteHostId, randomUserIdentifier, migratedClientInputId); - GetConnection()->SendReliablePacket(clientMigration); + GetMultiplayer()->SendNotifyClientMigrationEvent(GetConnection()->GetConnectionId(), remoteHostId, temporaryUserIdentifier, migratedClientInputId, m_controlledEntity.GetNetEntityId()); + // We need to send a MultiplayerPackets::ClientMigration packet to complete this process + // This happens inside MultiplayerSystemComponent, once we're certain the remote host has appropriately prepared m_controlledEntity = NetworkEntityHandle(); m_canSendUpdates = false; diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h index 1b4ee2cc04..4bd30a56a9 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h @@ -20,11 +20,12 @@ namespace Multiplayer ServerToClientConnectionData ( AzNetworking::IConnection* connection, - AzNetworking::IConnectionListener& connectionListener, - NetworkEntityHandle controlledEntity + AzNetworking::IConnectionListener& connectionListener ); ~ServerToClientConnectionData() override; + void SetControlledEntity(NetworkEntityHandle primaryPlayerEntity); + //! IConnectionData interface //! @{ ConnectionDataType GetConnectionDataType() const override; @@ -44,7 +45,7 @@ namespace Multiplayer private: void OnControlledEntityRemove(); - void OnControlledEntityMigration(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId, AzNetworking::ConnectionId connectionId); + void OnControlledEntityMigration(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId); void OnGameplayStarted(); EntityReplicationManager m_entityReplicationManager; diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl index e4348fe539..c166a2a961 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl @@ -18,7 +18,6 @@ namespace Multiplayer m_canSendUpdates = canSendUpdates; } - inline NetworkEntityHandle ServerToClientConnectionData::GetPrimaryPlayerEntity() { return m_controlledEntity; diff --git a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugHierarchyReporter.cpp b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugHierarchyReporter.cpp index 7c38a340eb..ad28307204 100644 --- a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugHierarchyReporter.cpp +++ b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugHierarchyReporter.cpp @@ -74,7 +74,7 @@ namespace Multiplayer { ImGui::Text("%s", entity->GetId().ToString().c_str()); ImGui::NextColumn(); - ImGui::Text("%u", GetMultiplayer()->GetNetworkEntityManager()->GetNetEntityIdById(entity->GetId())); + ImGui::Text("%llu", static_cast(GetMultiplayer()->GetNetworkEntityManager()->GetNetEntityIdById(entity->GetId()))); ImGui::NextColumn(); ImGui::Text("%s", entity->GetName().c_str()); ImGui::NextColumn(); diff --git a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp index b23bc677f0..4ff78d3815 100644 --- a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp @@ -28,24 +28,29 @@ namespace Multiplayer ->Version(1); } } + void MultiplayerDebugSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { provided.push_back(AZ_CRC_CE("MultiplayerDebugSystemComponent")); } + void MultiplayerDebugSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) { ; } + void MultiplayerDebugSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatbile) { incompatbile.push_back(AZ_CRC_CE("MultiplayerDebugSystemComponent")); } + void MultiplayerDebugSystemComponent::Activate() { #ifdef IMGUI_ENABLED ImGui::ImGuiUpdateListenerBus::Handler::BusConnect(); #endif } + void MultiplayerDebugSystemComponent::Deactivate() { #ifdef IMGUI_ENABLED @@ -75,6 +80,7 @@ namespace Multiplayer ImGui::EndMenu(); } } + void AccumulatePerSecondValues(const MultiplayerStats& stats, const MultiplayerStats::Metric& metric, float& outCallsPerSecond, float& outBytesPerSecond) { uint64_t summedCalls = 0; @@ -107,6 +113,7 @@ namespace Multiplayer ImGui::Text("%11.2f", bytesPerSecond); return open; } + bool DrawSummaryRow(const char* name, const MultiplayerStats& stats) { const MultiplayerStats::Metric propertyUpdatesSent = stats.CalculateTotalPropertyUpdateSentMetrics(); @@ -123,6 +130,7 @@ namespace Multiplayer AccumulatePerSecondValues(stats, rpcsRecv, callsPerSecond, bytesPerSecond); return DrawMetricsRow(name, true, totalCalls, totalBytes, callsPerSecond, bytesPerSecond); } + bool DrawComponentRow(const char* name, const MultiplayerStats& stats, NetComponentId netComponentId) { const MultiplayerStats::Metric propertyUpdatesSent = stats.CalculateComponentPropertyUpdateSentMetrics(netComponentId); @@ -139,6 +147,7 @@ namespace Multiplayer AccumulatePerSecondValues(stats, rpcsRecv, callsPerSecond, bytesPerSecond); return DrawMetricsRow(name, true, totalCalls, totalBytes, callsPerSecond, bytesPerSecond); } + void DrawComponentDetails(const MultiplayerStats& stats, NetComponentId netComponentId) { MultiplayerComponentRegistry* componentRegistry = GetMultiplayerComponentRegistry(); @@ -503,4 +512,3 @@ void OnDebugEntities_ShowBandwidth_Changed(const bool& showBandwidth) AZ::Interface::Get()->HideEntityBandwidthDebugOverlay(); } } - diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index 0316a9892f..6df5e4611f 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -76,6 +77,7 @@ namespace Multiplayer "The address of the remote server or host to connect to"); AZ_CVAR(uint16_t, cl_serverport, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port of the remote host to connect to for game traffic"); AZ_CVAR(uint16_t, sv_port, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that this multiplayer gem will bind to for game traffic"); + AZ_CVAR(uint16_t, sv_portRange, 999, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The range of ports the host will incrementally attempt to bind to when initializing"); AZ_CVAR(AZ::CVarFixedString, sv_map, "nolevel", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The map the server should load"); AZ_CVAR(ProtocolType, sv_protocol, ProtocolType::Udp, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "This flag controls whether we use TCP or UDP for game networking"); AZ_CVAR(bool, sv_isDedicated, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether the host command creates an independent or client hosted server"); @@ -168,6 +170,7 @@ namespace Multiplayer AZ::ConsoleFunctorFlags flags, AZ::ConsoleInvokedFrom invokedFrom ) { OnConsoleCommandInvoked(command, args, flags, invokedFrom); }) + , m_autonomousEntityReplicatorCreatedHandler([this]([[maybe_unused]] NetEntityId netEntityId) { OnAutonomousEntityReplicatorCreated(); }) { AZ::Interface::Register(this); } @@ -205,8 +208,23 @@ namespace Multiplayer bool MultiplayerSystemComponent::StartHosting(uint16_t port, bool isDedicated) { - InitializeMultiplayer(isDedicated ? MultiplayerAgentType::DedicatedServer : MultiplayerAgentType::ClientServer); - return m_networkInterface->Listen(port); + if (port != sv_port) + { + sv_port = port; + } + + const uint16_t maxPort = sv_port + sv_portRange; + while (sv_port <= maxPort) + { + if (m_networkInterface->Listen(sv_port)) + { + InitializeMultiplayer(isDedicated ? MultiplayerAgentType::DedicatedServer : MultiplayerAgentType::ClientServer); + return true; + } + AZLOG_WARN("Failed to start listening on port %u, port is in use?", static_cast(sv_port)); + sv_port = sv_port + 1; + } + return false; } bool MultiplayerSystemComponent::Connect(const AZStd::string& remoteAddress, uint16_t port) @@ -328,6 +346,11 @@ namespace Multiplayer void MultiplayerSystemComponent::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { + if (bg_multiplayerDebugDraw) + { + m_networkEntityManager.DebugDraw(); + } + const AZ::TimeMs deltaTimeMs = aznumeric_cast(static_cast(deltaTime * 1000.0f)); const AZ::TimeMs serverRateMs = static_cast(sv_serverSendRateMs); const float serverRateSeconds = static_cast(serverRateMs) / 1000.0f; @@ -412,11 +435,6 @@ namespace Multiplayer { m_networkInterface->GetConnectionSet().VisitConnections(visitor); } - - if (bg_multiplayerDebugDraw) - { - m_networkEntityManager.DebugDraw(); - } } int MultiplayerSystemComponent::GetTickOrder() @@ -487,17 +505,39 @@ namespace Multiplayer auto visitor = [](IConnection& connection) { connection.Disconnect(DisconnectReason::TerminatedByUser, TerminationEndpoint::Local); }; m_networkInterface->GetConnectionSet().VisitConnections(visitor); return true; - } + } } reinterpret_cast(connection->GetUserData())->SetProviderTicket(packet.GetTicket().c_str()); + // Hosts will spawn a new default player prefab for the user that just connected + if (GetAgentType() == MultiplayerAgentType::ClientServer + || GetAgentType() == MultiplayerAgentType::DedicatedServer) + { + // We use a temporary userId over the clients address so we can maintain client lookups even in the event of wifi handoff + NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(packet.GetTemporaryUserId()); + EnableAutonomousControl(controlledEntity, connection->GetConnectionId()); + + ServerToClientConnectionData* connectionData = reinterpret_cast(connection->GetUserData()); + AZStd::unique_ptr window = AZStd::make_unique(controlledEntity, connection); + connectionData->GetReplicationManager().SetReplicationWindow(AZStd::move(window)); + connectionData->SetControlledEntity(controlledEntity); + + // If this is a migrate or rejoin, immediately ready the connection for updates + if (packet.GetTemporaryUserId() != 0) + { + connectionData->SetCanSendUpdates(true); + } + } + if (connection->SendReliablePacket(MultiplayerPackets::Accept(sv_map))) { reinterpret_cast(connection->GetUserData())->SetDidHandshake(true); - - // Sync our console - ConsoleReplicator consoleReplicator(connection); - AZ::Interface::Get()->VisitRegisteredFunctors([&consoleReplicator](AZ::ConsoleFunctorBase* functor) { consoleReplicator.Visit(functor); }); + if (packet.GetTemporaryUserId() == 0) + { + // Sync our console + ConsoleReplicator consoleReplicator(connection); + AZ::Interface::Get()->VisitRegisteredFunctors([&consoleReplicator](AZ::ConsoleFunctorBase* functor) { consoleReplicator.Visit(functor); }); + } return true; } return false; @@ -511,10 +551,26 @@ namespace Multiplayer ) { reinterpret_cast(connection->GetUserData())->SetDidHandshake(true); - AZ::CVarFixedString commandString = "sv_map " + packet.GetMap(); - AZ::Interface::Get()->PerformCommand(commandString.c_str()); - AZ::CVarFixedString loadLevelString = "LoadLevel " + packet.GetMap(); - AZ::Interface::Get()->PerformCommand(loadLevelString.c_str()); + if (m_temporaryUserIdentifier == 0) + { + AZ::CVarFixedString commandString = "sv_map " + packet.GetMap(); + AZ::Interface::Get()->PerformCommand(commandString.c_str()); + AZ::CVarFixedString loadLevelString = "LoadLevel " + packet.GetMap(); + AZ::Interface::Get()->PerformCommand(loadLevelString.c_str()); + } + else + { + // Bypass map loading and immediately ready the connection for updates + IConnectionData* connectionData = reinterpret_cast(connection->GetUserData()); + if (connectionData) + { + connectionData->SetCanSendUpdates(true); + + // @nt: TODO - delete once dropped RPC problem fixed + // Connection has migrated, we are now waiting for the autonomous entity replicator to be created + connectionData->GetReplicationManager().AddAutonomousEntityReplicatorCreatedHandler(m_autonomousEntityReplicatorCreatedHandler); + } + } m_serverAcceptanceReceivedEvent.Signal(); return true; @@ -637,13 +693,17 @@ namespace Multiplayer // Store the temporary user identifier so we can transmit it with our next Connect packet // The new server will use this to re-attach our set of autonomous entities + m_temporaryUserIdentifier = packet.GetTemporaryUserIdentifier(); // Disconnect our existing server connection auto visitor = [](IConnection& connection) { connection.Disconnect(DisconnectReason::ClientMigrated, TerminationEndpoint::Local); }; m_networkInterface->GetConnectionSet().VisitConnections(visitor); AZLOG_INFO("Migrating to new server shard"); m_clientMigrationStartEvent.Signal(packet.GetLastClientInputId()); - m_networkInterface->Connect(packet.GetRemoteServerAddress()); + if (m_networkInterface->Connect(packet.GetRemoteServerAddress()) == AzNetworking::InvalidConnectionId) + { + AZLOG_ERROR("Failed to connect to new host during client migration event"); + } return true; } @@ -668,7 +728,7 @@ namespace Multiplayer providerTicket = m_pendingConnectionTickets.front(); m_pendingConnectionTickets.pop(); } - connection->SendReliablePacket(MultiplayerPackets::Connect(0, providerTicket.c_str())); + connection->SendReliablePacket(MultiplayerPackets::Connect(0, m_temporaryUserIdentifier, providerTicket.c_str())); } else { @@ -681,20 +741,10 @@ namespace Multiplayer m_connectionAcquiredEvent.Signal(datum); } - // Hosts will spawn a new default player prefab for the user that just connected if (GetAgentType() == MultiplayerAgentType::ClientServer || GetAgentType() == MultiplayerAgentType::DedicatedServer) { - NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(); - if (controlledEntity.Exists()) - { - controlledEntity.GetNetBindComponent()->SetOwningConnectionId(connection->GetConnectionId()); - } - controlledEntity.Activate(); - - connection->SetUserData(new ServerToClientConnectionData(connection, *this, controlledEntity)); - AZStd::unique_ptr window = AZStd::make_unique(controlledEntity, connection); - reinterpret_cast(connection->GetUserData())->GetReplicationManager().SetReplicationWindow(AZStd::move(window)); + connection->SetUserData(new ServerToClientConnectionData(connection, *this)); } else { @@ -716,9 +766,9 @@ namespace Multiplayer void MultiplayerSystemComponent::OnDisconnect(AzNetworking::IConnection* connection, DisconnectReason reason, TerminationEndpoint endpoint) { - const char* endpointString = (endpoint == TerminationEndpoint::Local) ? "Disconnecting" : "Remote host disconnected"; + const char* endpointString = (endpoint == TerminationEndpoint::Local) ? "Disconnecting" : "Remotely disconnected"; AZStd::string reasonString = ToString(reason); - AZLOG_INFO("%s due to %s from remote address: %s", endpointString, reasonString.c_str(), connection->GetRemoteAddress().GetString().c_str()); + AZLOG_INFO("%s from remote address %s due to %s", endpointString, connection->GetRemoteAddress().GetString().c_str(), reasonString.c_str()); // The client is disconnecting if (m_agentType == MultiplayerAgentType::Client) @@ -799,12 +849,8 @@ namespace Multiplayer // Spawn the default player for this host since the host is also a player (not a dedicated server) if (m_agentType == MultiplayerAgentType::ClientServer) { - NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(); - if (NetBindComponent* controlledEntityNetBindComponent = controlledEntity.GetNetBindComponent()) - { - controlledEntityNetBindComponent->SetAllowAutonomy(true); - } - controlledEntity.Activate(); + NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(0); + EnableAutonomousControl(controlledEntity, AzNetworking::InvalidConnectionId); } AZLOG_INFO("Multiplayer operating in %s mode", GetEnumString(m_agentType)); @@ -855,9 +901,9 @@ namespace Multiplayer handler.Connect(m_shutdownEvent); } - void MultiplayerSystemComponent::SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId) + void MultiplayerSystemComponent::SendNotifyClientMigrationEvent(AzNetworking::ConnectionId connectionId, const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) { - m_notifyClientMigrationEvent.Signal(hostId, userIdentifier, lastClientInputId); + m_notifyClientMigrationEvent.Signal(connectionId, hostId, userIdentifier, lastClientInputId, controlledEntityId); } void MultiplayerSystemComponent::SendNotifyEntityMigrationEvent(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) @@ -911,6 +957,22 @@ namespace Multiplayer return m_filterEntityManager; } + void MultiplayerSystemComponent::RegisterPlayerIdentifierForRejoin(uint64_t temporaryUserIdentifier, NetEntityId controlledEntityId) + { + m_playerRejoinData[temporaryUserIdentifier] = controlledEntityId; + } + + void MultiplayerSystemComponent::CompleteClientMigration(uint64_t temporaryUserIdentifier, AzNetworking::ConnectionId connectionId, const HostId& publicHostId, ClientInputId migratedClientInputId) + { + IConnection* connection = m_networkInterface->GetConnectionSet().GetConnection(connectionId); + if (connection != nullptr) // Make sure the player has not disconnected since the start of migration + { + // Tell the client who to join + MultiplayerPackets::ClientMigration clientMigration(publicHostId, temporaryUserIdentifier, migratedClientInputId); + connection->SendReliablePacket(clientMigration); + } + } + void MultiplayerSystemComponent::SetShouldSpawnNetworkEntities(bool value) { m_spawnNetboundEntities = value; @@ -1041,6 +1103,13 @@ namespace Multiplayer m_cvarCommands.PushBackItem(AZStd::move(replicateString)); } + void MultiplayerSystemComponent::OnAutonomousEntityReplicatorCreated() + { + m_autonomousEntityReplicatorCreatedHandler.Disconnect(); + //m_networkEntityManager.GetNetworkEntityAuthorityTracker()->ResetTimeoutTime(AZ::TimeMs{ 2000 }); + m_clientMigrationEndEvent.Signal(); + } + void MultiplayerSystemComponent::ExecuteConsoleCommandList(IConnection* connection, const AZStd::fixed_vector& commands) { AZ::IConsole* console = AZ::Interface::Get(); @@ -1052,15 +1121,25 @@ namespace Multiplayer } } - NetworkEntityHandle MultiplayerSystemComponent::SpawnDefaultPlayerPrefab() + NetworkEntityHandle MultiplayerSystemComponent::SpawnDefaultPlayerPrefab(uint64_t temporaryUserIdentifier) { + const auto node = m_playerRejoinData.find(temporaryUserIdentifier); + if (node != m_playerRejoinData.end()) + { + return m_networkEntityManager.GetNetworkEntityTracker()->Get(node->second); + } + // make sure the player prefab path is lowercase (how it's stored in the cache folder) auto sv_defaultPlayerSpawnAssetLowerCase = static_cast(sv_defaultPlayerSpawnAsset); AZStd::to_lower(sv_defaultPlayerSpawnAssetLowerCase.begin(), sv_defaultPlayerSpawnAssetLowerCase.end()); - - PrefabEntityId playerPrefabEntityId(AZ::Name(sv_defaultPlayerSpawnAssetLowerCase.c_str())); + PrefabEntityId playerPrefabEntityId(AZ::Name(static_cast(sv_defaultPlayerSpawnAssetLowerCase).c_str())); INetworkEntityManager::EntityList entityList = m_networkEntityManager.CreateEntitiesImmediate(playerPrefabEntityId, NetEntityRole::Authority, AZ::Transform::CreateIdentity(), Multiplayer::AutoActivate::DoNotActivate); + for (NetworkEntityHandle subEntity : entityList) + { + subEntity.Activate(); + } + NetworkEntityHandle controlledEntity; if (!entityList.empty()) { @@ -1069,11 +1148,45 @@ namespace Multiplayer return controlledEntity; } + void MultiplayerSystemComponent::EnableAutonomousControl(NetworkEntityHandle entityHandle, AzNetworking::ConnectionId connectionId) + { + if (!entityHandle.Exists()) + { + AZLOG_WARN("Attempting to enable autonomous control for an invalid entity"); + return; + } + + entityHandle.GetNetBindComponent()->SetOwningConnectionId(connectionId); + if (connectionId == InvalidConnectionId) + { + entityHandle.GetNetBindComponent()->SetAllowAutonomy(true); + } + + auto* hierarchyComponent = entityHandle.FindComponent(); + if (hierarchyComponent != nullptr) + { + for (AZ::Entity* subEntity : hierarchyComponent->GetHierarchicalEntities()) + { + NetworkEntityHandle subEntityHandle = NetworkEntityHandle(subEntity); + NetBindComponent* subEntityNetBindComponent = subEntityHandle.GetNetBindComponent(); + + if (subEntityNetBindComponent != nullptr) + { + subEntityNetBindComponent->SetOwningConnectionId(connectionId); + if (connectionId == InvalidConnectionId) + { + subEntityNetBindComponent->SetAllowAutonomy(true); + } + } + } + } + } + void host([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments) { if (!AZ::Interface::Get()->StartHosting(sv_port, sv_isDedicated)) { - AZLOG_ERROR("Failed to start listening on port %u, port is in use?", static_cast(sv_port)); + AZLOG_ERROR("Failed to start listening on any allocated port"); } } AZ_CONSOLEFREEFUNC(host, AZ::ConsoleFunctorFlags::DontReplicate, "Opens a multiplayer connection as a host for other clients to connect to"); diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index 568d5f7801..87d084d5bc 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -123,7 +123,7 @@ namespace Multiplayer void AddSessionInitHandler(SessionInitEvent::Handler& handler) override; void AddSessionShutdownHandler(SessionShutdownEvent::Handler& handler) override; void AddServerAcceptanceReceivedHandler(ServerAcceptanceReceivedEvent::Handler& handler) override; - void SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId) override; + void SendNotifyClientMigrationEvent(AzNetworking::ConnectionId connectionId, const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) override; void SendNotifyEntityMigrationEvent(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) override; void SendReadyForEntityUpdates(bool readyForEntityUpdates) override; AZ::TimeMs GetCurrentHostTimeMs() const override; @@ -132,6 +132,8 @@ namespace Multiplayer INetworkEntityManager* GetNetworkEntityManager() override; void SetFilterEntityManager(IFilterEntityManager* entityFilter) override; IFilterEntityManager* GetFilterEntityManager() override; + void RegisterPlayerIdentifierForRejoin(uint64_t temporaryUserIdentifier, NetEntityId controlledEntityId) override; + void CompleteClientMigration(uint64_t temporaryUserIdentifier, AzNetworking::ConnectionId connectionId, const HostId& publicHostId, ClientInputId migratedClientInputId) override; void SetShouldSpawnNetworkEntities(bool value) override; bool GetShouldSpawnNetworkEntities() const override; //! @} @@ -145,9 +147,11 @@ namespace Multiplayer void TickVisibleNetworkEntities(float deltaTime, float serverRateSeconds); void OnConsoleCommandInvoked(AZStd::string_view command, const AZ::ConsoleCommandContainer& args, AZ::ConsoleFunctorFlags flags, AZ::ConsoleInvokedFrom invokedFrom); + void OnAutonomousEntityReplicatorCreated(); void ExecuteConsoleCommandList(AzNetworking::IConnection* connection, const AZStd::fixed_vector& commands); - NetworkEntityHandle SpawnDefaultPlayerPrefab(); - + NetworkEntityHandle SpawnDefaultPlayerPrefab(uint64_t temporaryUserIdentifier); + void EnableAutonomousControl(NetworkEntityHandle entityHandle, AzNetworking::ConnectionId connectionId); + AZ_CONSOLEFUNC(MultiplayerSystemComponent, DumpStats, AZ::ConsoleFunctorFlags::Null, "Dumps stats for the current multiplayer session"); AzNetworking::INetworkInterface* m_networkInterface = nullptr; @@ -170,12 +174,16 @@ namespace Multiplayer ClientMigrationEndEvent m_clientMigrationEndEvent; NotifyClientMigrationEvent m_notifyClientMigrationEvent; NotifyEntityMigrationEvent m_notifyEntityMigrationEvent; + AZ::Event::Handler m_autonomousEntityReplicatorCreatedHandler; AZStd::queue m_pendingConnectionTickets; + AZStd::unordered_map m_playerRejoinData; AZ::TimeMs m_lastReplicatedHostTimeMs = AZ::TimeMs{ 0 }; HostFrameId m_lastReplicatedHostFrameId = HostFrameId(0); + uint64_t m_temporaryUserIdentifier = 0; // Used in the event of a migration or rejoin + double m_serverSendAccumulator = 0.0; float m_renderBlendFactor = 0.0f; float m_tickFactor = 0.0f; diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp index 6f65d01e50..fa099842c5 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp @@ -47,6 +47,9 @@ namespace Multiplayer , m_entityExitDomainEventHandler([this](const ConstNetworkEntityHandle& entityHandle) { OnEntityExitDomain(entityHandle); }) , m_notifyEntityMigrationHandler([this](const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) { OnPostEntityMigration(entityHandle, remoteHostId); }) { + // Set up our remote host identifier, by default we use the IP address of the remote host + m_remoteHostId = connection.GetRemoteAddress(); + // Our max payload size is whatever is passed in, minus room for a udp packetheader m_maxPayloadSize = connection.GetConnectionMtu() - UdpPacketHeaderSerializeSize - ReplicationManagerPacketOverhead; @@ -62,12 +65,10 @@ namespace Multiplayer networkEntityManager->AddEntityExitDomainHandler(m_entityExitDomainEventHandler); } - GetMultiplayer()->AddNotifyEntityMigrationEventHandler(m_notifyEntityMigrationHandler); - } - - void EntityReplicationManager::SetRemoteHostId(const HostId& hostId) - { - m_remoteHostId = hostId; + if (m_updateMode == Mode::LocalServerToRemoteServer) + { + GetMultiplayer()->AddNotifyEntityMigrationEventHandler(m_notifyEntityMigrationHandler); + } } const HostId& EntityReplicationManager::GetRemoteHostId() const @@ -258,8 +259,8 @@ namespace Multiplayer { AZLOG_WARN ( - "Serializing extremely large entity (%u) - MaxPayload: %d NeededSize %d", - aznumeric_cast(replicator->GetEntityHandle().GetNetEntityId()), + "Serializing extremely large entity (%llu) - MaxPayload: %d NeededSize %d", + aznumeric_cast(replicator->GetEntityHandle().GetNetEntityId()), m_maxPayloadSize, nextMessageSize ); @@ -364,15 +365,29 @@ namespace Multiplayer const bool changedRemoteRole = (remoteNetworkRole != entityReplicator->GetRemoteNetworkRole()); // Check if we've changed our bound local role - this can occur when we gain Autonomous or lose Autonomous on a client bool changedLocalRole(false); - if (AZ::Entity* localEnt = entityReplicator->GetEntityHandle().GetEntity()) + NetBindComponent* netBindComponent = entityReplicator->GetEntityHandle().GetNetBindComponent(); + if (netBindComponent != nullptr) { - NetBindComponent* netBindComponent = entityReplicator->GetEntityHandle().GetNetBindComponent(); - AZ_Assert(netBindComponent != nullptr, "No NetBindComponent"); changedLocalRole = (netBindComponent->GetNetEntityRole() != entityReplicator->GetBoundLocalNetworkRole()); } if (changedRemoteRole || changedLocalRole) { + const AZ::u64 intEntityId = static_cast(netBindComponent->GetNetEntityId()); + const char* entityName = entityReplicator->GetEntityHandle().GetEntity()->GetName().c_str(); + if (changedLocalRole) + { + const char* oldRoleString = GetEnumString(entityReplicator->GetRemoteNetworkRole()); + const char* newRoleString = GetEnumString(remoteNetworkRole); + AZLOG(NET_ReplicatorRoles, "Replicator %s(%llu) changed local role, old role = %s, new role = %s", entityName, intEntityId, oldRoleString, newRoleString); + } + if (changedRemoteRole) + { + const char* oldRoleString = GetEnumString(entityReplicator->GetBoundLocalNetworkRole()); + const char* newRoleString = GetEnumString(netBindComponent->GetNetEntityRole()); + AZLOG(NET_ReplicatorRoles, "Replicator %s(%llu) changed remote role, old role = %s, new role = %s", entityName, intEntityId, oldRoleString, newRoleString); + } + // If we changed roles, we need to reset everything if (!entityReplicator->IsMarkedForRemoval()) { @@ -387,8 +402,8 @@ namespace Multiplayer AZLOG ( NET_RepDeletes, - "Reinited replicator for %u from remote host %s role %d", - entityHandle.GetNetEntityId(), + "Reinited replicator for netEntityId %llu from remote host %s role %d", + static_cast(entityHandle.GetNetEntityId()), GetRemoteHostId().GetString().c_str(), aznumeric_cast(remoteNetworkRole) ); @@ -404,8 +419,8 @@ namespace Multiplayer AZLOG ( NET_RepDeletes, - "Added replicator for %u from remote host %s role %d", - entityHandle.GetNetEntityId(), + "Added replicator for netEntityId %llu from remote host %s role %d", + static_cast(entityHandle.GetNetEntityId()), GetRemoteHostId().GetString().c_str(), aznumeric_cast(remoteNetworkRole) ); @@ -413,7 +428,7 @@ namespace Multiplayer } else { - AZLOG_ERROR("Failed to add entity replicator, entity does not exist, entity id %u", entityHandle.GetNetEntityId()); + AZLOG_ERROR("Failed to add entity replicator, entity does not exist, netEntityId %llu", static_cast(entityHandle.GetNetEntityId())); AZ_Assert(false, "Failed to add entity replicator, entity does not exist"); } return entityReplicator; @@ -502,24 +517,20 @@ namespace Multiplayer { if (entityReplicator->IsMarkedForRemoval()) { - AZLOG(NET_RepDeletes, "Got a replicator delete message that is a duplicate id %u remote host %s", updateMessage.GetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Got a replicator delete message that is a duplicate id %llu remote host %s", static_cast(updateMessage.GetEntityId()), GetRemoteHostId().GetString().c_str()); } else if (entityReplicator->OwnsReplicatorLifetime()) { // This can occur if we migrate entities quickly - if this is a replicator from C to A, A migrates to B, B then migrates to C, and A's delete replicator has not arrived at C - AZLOG(NET_RepDeletes, "Got a replicator delete message for a replicator we own id %u remote host %s", updateMessage.GetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Got a replicator delete message for a replicator we own id %llu remote host %s", static_cast(updateMessage.GetEntityId()), GetRemoteHostId().GetString().c_str()); } else { shouldDeleteEntity = true; entityReplicator->MarkForRemoval(); - AZLOG(NET_RepDeletes, "Deleting replicater for entity id %u remote host %s", updateMessage.GetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Deleting replicater for entity id %llu remote host %s", static_cast(updateMessage.GetEntityId()), GetRemoteHostId().GetString().c_str()); } } - else - { - shouldDeleteEntity = updateMessage.GetTakeOwnership(); - } // Handle entity cleanup if (shouldDeleteEntity) @@ -529,17 +540,17 @@ namespace Multiplayer { if (updateMessage.GetWasMigrated()) { - AZLOG(NET_RepDeletes, "Leaving id %u using timeout remote host %s", entity.GetNetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Leaving id %llu using timeout remote host %s", static_cast(entity.GetNetEntityId()), GetRemoteHostId().GetString().c_str()); } else { - AZLOG(NET_RepDeletes, "Deleting entity id %u remote host %s", entity.GetNetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Deleting entity id %llu remote host %s", static_cast(entity.GetNetEntityId()), GetRemoteHostId().GetString().c_str()); GetNetworkEntityManager()->MarkForRemoval(entity); } } else { - AZLOG(NET_RepDeletes, "Trying to delete entity id %u remote host %s, but it has been removed", entity.GetNetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Trying to delete entity id %llu remote host %s, but it has been removed", static_cast(entity.GetNetEntityId()), GetRemoteHostId().GetString().c_str()); } } @@ -583,9 +594,9 @@ namespace Multiplayer NetBindComponent* netBindComponent = replicatorEntity.GetNetBindComponent(); AZ_Assert(netBindComponent != nullptr, "No NetBindComponent"); - if (createEntity) + if (netBindComponent->GetOwningConnectionId() != invokingConnection->GetConnectionId()) { - // Always set our invoking connectionId for any newly created entities, since this connection now 'owns' them from a rewind perspective + // Always ensure our owning connectionId is correct for correct rewind behaviour netBindComponent->SetOwningConnectionId(invokingConnection->GetConnectionId()); } @@ -595,10 +606,11 @@ namespace Multiplayer AZ_Assert(localNetworkRole != NetEntityRole::Authority, "UpdateMessage trying to set local role to Authority, this should only happen via migration"); AZLOG_INFO ( - "EntityReplicationManager: Changing network role on entity %u, old role %u new role %u", - aznumeric_cast(netEntityId), - aznumeric_cast(netBindComponent->GetNetEntityRole()), - aznumeric_cast(localNetworkRole) + "EntityReplicationManager: Changing network role on entity %s(%llu), old role %s new role %s", + replicatorEntity.GetEntity()->GetName().c_str(), + aznumeric_cast(netEntityId), + GetEnumString(netBindComponent->GetNetEntityRole()), + GetEnumString(localNetworkRole) ); if (NetworkRoleHasController(localNetworkRole)) @@ -708,9 +720,9 @@ namespace Multiplayer AZLOG_WARN ( "Dropping Packet and LocalServerToRemoteClient connection, unexpected packet " - "LocalShard=%s EntityId=%u RemoteNetworkRole=%u BoundLocalNetworkRole=%u ActualNetworkRole=%u IsMarkedForRemoval=%s", + "LocalShard=%s EntityId=%llu RemoteNetworkRole=%u BoundLocalNetworkRole=%u ActualNetworkRole=%u IsMarkedForRemoval=%s", GetNetworkEntityManager()->GetHostId().GetString().c_str(), - aznumeric_cast(entityReplicator->GetEntityHandle().GetNetEntityId()), + aznumeric_cast(entityReplicator->GetEntityHandle().GetNetEntityId()), aznumeric_cast(entityReplicator->GetRemoteNetworkRole()), aznumeric_cast(entityReplicator->GetBoundLocalNetworkRole()), aznumeric_cast(entityReplicator->GetNetBindComponent()->GetNetEntityRole()), @@ -760,13 +772,13 @@ namespace Multiplayer result = UpdateValidationResult::DropMessage; if (updateMessage.GetIsDelete()) { - AZLOG(NET_RepDeletes, "EntityReplicationManager: Received old DeleteProxy message for entity id %u, sequence %d latest sequence %d from remote host %s", - updateMessage.GetEntityId(), (uint32_t)packetId, (uint32_t)propSubscriber->GetLastReceivedPacketId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "EntityReplicationManager: Received old DeleteProxy message for entity id %llu, sequence %d latest sequence %d from remote host %s", + (AZ::u64)updateMessage.GetEntityId(), (uint32_t)packetId, (uint32_t)propSubscriber->GetLastReceivedPacketId(), GetRemoteHostId().GetString().c_str()); } else { - AZLOG(NET_RepUpdate, "EntityReplicationManager: Received old PropertyChangeMessage message for entity id %u, sequence %d latest sequence %d from remote host %s", - updateMessage.GetEntityId(), (uint32_t)packetId, (uint32_t)propSubscriber->GetLastReceivedPacketId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepUpdate, "EntityReplicationManager: Received old PropertyChangeMessage message for entity id %llu, sequence %d latest sequence %d from remote host %s", + (AZ::u64)updateMessage.GetEntityId(), (uint32_t)packetId, (uint32_t)propSubscriber->GetLastReceivedPacketId(), GetRemoteHostId().GetString().c_str()); } } } @@ -853,10 +865,10 @@ namespace Multiplayer { AZLOG_INFO ( - "EntityReplicationManager: Dropping remote RPC message for component %s of rpc index %s, entityId %u has already been deleted", + "EntityReplicationManager: Dropping remote RPC message for component %s of rpc index %s, entityId %llu has already been deleted", GetMultiplayerComponentRegistry()->GetComponentName(message.GetComponentId()), GetMultiplayerComponentRegistry()->GetComponentRpcName(message.GetComponentId(), message.GetRpcIndex()), - message.GetEntityId() + static_cast(message.GetEntityId()) ); return false; } @@ -1113,7 +1125,7 @@ namespace Multiplayer if (m_updateMode == EntityReplicationManager::Mode::LocalServerToRemoteServer) { - netBindComponent->NotifyServerMigration(GetRemoteHostId(), GetConnection().GetConnectionId()); + netBindComponent->NotifyServerMigration(GetRemoteHostId()); } bool didSucceed = true; @@ -1145,7 +1157,7 @@ namespace Multiplayer AZ_Assert(didSucceed, "Failed to migrate entity from server"); m_sendMigrateEntityEvent.Signal(m_connection, message); - AZLOG(NET_RepDeletes, "Migration packet sent %u to remote host %s", netEntityId, GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Migration packet sent %llu to remote host %s", static_cast(netEntityId), GetRemoteHostId().GetString().c_str()); // Notify all other EntityReplicationManagers that this entity has migrated so they can adjust their own replicators given our new proxy status GetMultiplayer()->SendNotifyEntityMigrationEvent(entityHandle, GetRemoteHostId()); @@ -1201,7 +1213,7 @@ namespace Multiplayer // Change the role on the replicator AddEntityReplicator(entityHandle, NetEntityRole::Server); - AZLOG(NET_RepDeletes, "Handle Migration %u new authority from remote host %s", entityHandle.GetNetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Handle Migration %llu new authority from remote host %s", static_cast(entityHandle.GetNetEntityId()), GetRemoteHostId().GetString().c_str()); return true; } diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp index 05001b5960..67527bf962 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp @@ -103,8 +103,8 @@ namespace Multiplayer AZ_Assert ( m_boundLocalNetworkRole != m_remoteNetworkRole, - "Invalid configuration detected, bound local role must differ from remote network role Role: %d", - aznumeric_cast(m_boundLocalNetworkRole) + "Invalid configuration detected, bound local role must differ from remote network role: %s", + GetEnumString(m_boundLocalNetworkRole) ); if (RemoteManagerOwnsEntityLifetime()) @@ -176,7 +176,6 @@ namespace Multiplayer switch (GetBoundLocalNetworkRole()) { case NetEntityRole::Authority: - { if (GetRemoteNetworkRole() == NetEntityRole::Client || GetRemoteNetworkRole() == NetEntityRole::Autonomous) { m_onSendRpcHandler.Connect(netBindComponent->GetSendAuthorityToClientRpcEvent()); @@ -189,10 +188,8 @@ namespace Multiplayer { m_onForwardRpcHandler.Connect(netBindComponent->GetSendAuthorityToClientRpcEvent()); } - } - break; + break; case NetEntityRole::Server: - { if (GetRemoteNetworkRole() == NetEntityRole::Authority) { m_onSendRpcHandler.Connect(netBindComponent->GetSendServerToAuthorityRpcEvent()); @@ -204,23 +201,21 @@ namespace Multiplayer // Listen for these to forward the rpc along to the other Client replicators m_onSendRpcHandler.Connect(netBindComponent->GetSendAuthorityToClientRpcEvent()); } - // NOTE: e_Autonomous is not connected to e_ServerProxy, it is always connected to an e_Authority - AZ_Assert(GetRemoteNetworkRole() != NetEntityRole::Autonomous, "Unexpected autonomous remote role") - } - break; + else if (GetRemoteNetworkRole() == NetEntityRole::Autonomous) + { + // NOTE: Autonomous is not connected to ServerProxy, it is always connected to an Authority + AZ_Assert(false, "Unexpected autonomous remote role") + } + break; case NetEntityRole::Client: - { // Nothing allowed, no Client to Server communication - } - break; + break; case NetEntityRole::Autonomous: - { if (GetRemoteNetworkRole() == NetEntityRole::Authority) { m_onSendRpcHandler.Connect(netBindComponent->GetSendAutonomousToAuthorityRpcEvent()); } - } - break; + break; default: AZ_Assert(false, "Unexpected network role"); } @@ -252,22 +247,9 @@ namespace Multiplayer if (entity->GetState() != AZ::Entity::State::Init) { - AZLOG_WARN("Trying to activate an entity that is not in the Init state (%u)", GetEntityHandle().GetNetEntityId()); - } - - // First we need to make sure the transform component has been updated with the correct value prior to activation - // This is because vanilla az components may only depend on the transform component, not the multiplayer transform component - //if (auto* locationComponent = FindCommonComponent(GetEntityHandle())) - //{ - // AZ::Transform newTransform = locationComponent->GetTransform(); - // auto* transformComponent = entity->FindComponent(); - // if (transformComponent) - // { - // // We can't use EBus here since the TransFormBus does not get connected until the activate call below - // transformComponent->SetWorldTM(newTransform); - // } - //} - // Ugly, but this is the only time we need to call a non-const function on this entity + AZLOG_WARN("Trying to activate an entity that is not in the Init state (%llu)", static_cast(GetEntityHandle().GetNetEntityId())); + } + entity->Activate(); m_replicationManager.m_orphanedEntityRpcs.DispatchOrphanedRpcs(*this); @@ -281,8 +263,7 @@ namespace Multiplayer NetBindComponent* netBindComponent = m_netBindComponent; AZ_Assert(netBindComponent, "No Multiplayer::NetBindComponent"); - bool isAuthority = (GetBoundLocalNetworkRole() == NetEntityRole::Authority) - && (GetBoundLocalNetworkRole() == netBindComponent->GetNetEntityRole()); + bool isAuthority = (GetBoundLocalNetworkRole() == NetEntityRole::Authority) && (GetBoundLocalNetworkRole() == netBindComponent->GetNetEntityRole()); bool isClient = GetRemoteNetworkRole() == NetEntityRole::Client; bool isAutonomous = GetBoundLocalNetworkRole() == NetEntityRole::Autonomous; if (isAuthority || isClient || isAutonomous) @@ -296,10 +277,10 @@ namespace Multiplayer bool EntityReplicator::OwnsReplicatorLifetime() const { bool ret(false); - if (GetBoundLocalNetworkRole() == NetEntityRole::Authority - || (GetBoundLocalNetworkRole() == NetEntityRole::Server + if (GetBoundLocalNetworkRole() == NetEntityRole::Authority // Authority always owns lifetime + || (GetBoundLocalNetworkRole() == NetEntityRole::Server // Server also owns lifetime if the remote endpoint is a client of some form && (GetRemoteNetworkRole() == NetEntityRole::Client - || GetRemoteNetworkRole() == NetEntityRole::Autonomous))) + || GetRemoteNetworkRole() == NetEntityRole::Autonomous))) { ret = true; } @@ -309,10 +290,9 @@ namespace Multiplayer bool EntityReplicator::RemoteManagerOwnsEntityLifetime() const { bool isServer = (GetBoundLocalNetworkRole() == NetEntityRole::Server) - && (GetRemoteNetworkRole() == NetEntityRole::Authority); + && (GetRemoteNetworkRole() == NetEntityRole::Authority); bool isClient = (GetBoundLocalNetworkRole() == NetEntityRole::Client) - || (GetBoundLocalNetworkRole() == NetEntityRole::Autonomous); - + || (GetBoundLocalNetworkRole() == NetEntityRole::Autonomous); return isServer || isClient; } @@ -429,10 +409,8 @@ namespace Multiplayer if (const NetworkTransformComponent* networkTransform = entity->FindComponent()) { const NetEntityId parentId = networkTransform->GetParentEntityId(); - /* - * For root entities attached to a level, a network parent won't be set. - * In this case, this entity is the root entity of the hierarchy and it will be activated first. - */ + // For root entities attached to a level, a network parent won't be set. + // In this case, this entity is the root entity of the hierarchy and it will be activated first. if (parentId != InvalidNetEntityId) { ConstNetworkEntityHandle parentHandle = GetNetworkEntityManager()->GetEntity(parentId); @@ -452,9 +430,9 @@ namespace Multiplayer AZLOG ( NET_HierarchyActivationInfo, - "Hierchical entity %s asking for activation - waiting on the parent %u", + "Hierchical entity %s asking for activation - waiting on the parent %llu", entity->GetName().c_str(), - aznumeric_cast(parentId) + aznumeric_cast(parentId) ); return false; } @@ -472,19 +450,19 @@ namespace Multiplayer AZLOG ( NET_RepDeletes, - "Sending delete replicator id %u migrated %d to remote host %s", - aznumeric_cast(GetEntityHandle().GetNetEntityId()), + "Sending delete replicator id %llu migrated %d to remote host %s", + aznumeric_cast(GetEntityHandle().GetNetEntityId()), WasMigrated() ? 1 : 0, m_replicationManager.GetRemoteHostId().GetString().c_str() ); - return NetworkEntityUpdateMessage(GetEntityHandle().GetNetEntityId(), WasMigrated(), m_propertyPublisher->IsRemoteReplicatorEstablished()); + return NetworkEntityUpdateMessage(GetEntityHandle().GetNetEntityId(), WasMigrated()); } NetBindComponent* netBindComponent = GetNetBindComponent(); - //const bool sendSliceName = !m_propertyPublisher->IsRemoteReplicatorEstablished(); + const bool sendSliceName = !m_propertyPublisher->IsRemoteReplicatorEstablished(); NetworkEntityUpdateMessage updateMessage(GetRemoteNetworkRole(), GetEntityHandle().GetNetEntityId()); - //if (sendSliceName) + if (sendSliceName) { updateMessage.SetPrefabEntityId(netBindComponent->GetPrefabEntityId()); } @@ -553,42 +531,33 @@ namespace Multiplayer switch (entityRpcMessage.GetRpcDeliveryType()) { case RpcDeliveryType::AuthorityToClient: - { if (((GetBoundLocalNetworkRole() == NetEntityRole::Client) || (GetBoundLocalNetworkRole() == NetEntityRole::Autonomous)) && (GetRemoteNetworkRole() == NetEntityRole::Authority)) { // We are a local client, and we are connected to server, aka AuthorityToClient result = RpcValidationResult::HandleRpc; } - if ((GetBoundLocalNetworkRole() == NetEntityRole::Server) - && (GetRemoteNetworkRole() == NetEntityRole::Authority)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Server) && (GetRemoteNetworkRole() == NetEntityRole::Authority)) { // We are on a server, and we received this message from another server, therefore we should forward this to any connected clients result = RpcValidationResult::ForwardToClient; } - } - break; + break; case RpcDeliveryType::AuthorityToAutonomous: - { - if ((GetBoundLocalNetworkRole() == NetEntityRole::Autonomous) - && (GetRemoteNetworkRole() == NetEntityRole::Authority)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Autonomous) && (GetRemoteNetworkRole() == NetEntityRole::Authority)) { // We are an autonomous client, and we are connected to server, aka AuthorityToAutonomous result = RpcValidationResult::HandleRpc; } - if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) - && (GetRemoteNetworkRole() == NetEntityRole::Server)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) && (GetRemoteNetworkRole() == NetEntityRole::Server)) { // We are on a server, and we received this message from another server, therefore we should forward this to our autonomous player // This can occur if we've recently migrated result = RpcValidationResult::ForwardToAutonomous; } - } - break; + break; case RpcDeliveryType::AutonomousToAuthority: - { - if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) - && (GetRemoteNetworkRole() == NetEntityRole::Autonomous)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) && (GetRemoteNetworkRole() == NetEntityRole::Autonomous)) { if (IsMarkedForRemoval()) { @@ -610,12 +579,9 @@ namespace Multiplayer result = RpcValidationResult::HandleRpc; } } - } - break; + break; case RpcDeliveryType::ServerToAuthority: - { - if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) - && (GetRemoteNetworkRole() == NetEntityRole::Server)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) && (GetRemoteNetworkRole() == NetEntityRole::Server)) { // if we're marked for removal, then we should forward to whomever now owns this entity if (IsMarkedForRemoval()) @@ -638,9 +604,9 @@ namespace Multiplayer result = RpcValidationResult::HandleRpc; } } + break; } - break; - } + if (result == RpcValidationResult::DropRpcAndDisconnect) { bool isLocalServer = (GetBoundLocalNetworkRole() == NetEntityRole::Authority) || (GetBoundLocalNetworkRole() == NetEntityRole::Server); @@ -654,30 +620,29 @@ namespace Multiplayer { AZLOG_ERROR ( - "Dropping RPC and Connection EntityId=%u LocalRole=%u RemoteRole=%u RpcDeliveryType=%u ComponentId=%u RpcType=%u IsReliable=%s IsMarkedForRemoval=%s", - aznumeric_cast(m_entityHandle.GetNetEntityId()), - aznumeric_cast(GetBoundLocalNetworkRole()), - aznumeric_cast(GetRemoteNetworkRole()), + "Dropping RPC and Connection EntityId=%llu LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", + aznumeric_cast(m_entityHandle.GetNetEntityId()), + GetEnumString(GetBoundLocalNetworkRole()), + GetEnumString(GetRemoteNetworkRole()), aznumeric_cast(entityRpcMessage.GetRpcDeliveryType()), - aznumeric_cast(entityRpcMessage.GetComponentId()), - aznumeric_cast(entityRpcMessage.GetRpcIndex()), + GetMultiplayerComponentRegistry()->GetComponentRpcName(entityRpcMessage.GetComponentId(), entityRpcMessage.GetRpcIndex()), entityRpcMessage.GetReliability() == ReliabilityType::Reliable ? "true" : "false", IsMarkedForRemoval() ? "true" : "false" ); } } + if (result == RpcValidationResult::DropRpc) { AZLOG ( NET_Rpc, - "Dropping RPC EntityId=%u LocalRole=%u RemoteRole=%u RpcDeliveryType=%u ComponentId=%u RpcType=%u IsReliable=%s IsMarkedForRemoval=%s", - aznumeric_cast(m_entityHandle.GetNetEntityId()), - aznumeric_cast(GetBoundLocalNetworkRole()), - aznumeric_cast(GetRemoteNetworkRole()), + "Dropping RPC EntityId=%llu LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", + aznumeric_cast(m_entityHandle.GetNetEntityId()), + GetEnumString(GetBoundLocalNetworkRole()), + GetEnumString(GetRemoteNetworkRole()), aznumeric_cast(entityRpcMessage.GetRpcDeliveryType()), - aznumeric_cast(entityRpcMessage.GetComponentId()), - aznumeric_cast(entityRpcMessage.GetRpcIndex()), + GetMultiplayerComponentRegistry()->GetComponentRpcName(entityRpcMessage.GetComponentId(), entityRpcMessage.GetRpcIndex()), entityRpcMessage.GetReliability() == ReliabilityType::Reliable ? "true" : "false", IsMarkedForRemoval() ? "true" : "false" ); @@ -696,13 +661,12 @@ namespace Multiplayer { AZLOG_WARN ( - "Dropping RPC since entity deleted EntityId=%u LocalRole=%u RemoteRole=%u RpcDeliveryType=%u ComponentId=%u RpcType=%u IsReliable=%s IsMarkedForRemoval=%s", - aznumeric_cast(m_entityHandle.GetNetEntityId()), - aznumeric_cast(GetBoundLocalNetworkRole()), - aznumeric_cast(GetRemoteNetworkRole()), + "Dropping RPC since entity deleted EntityId=%llu LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", + aznumeric_cast(m_entityHandle.GetNetEntityId()), + GetEnumString(GetBoundLocalNetworkRole()), + GetEnumString(GetRemoteNetworkRole()), aznumeric_cast(entityRpcMessage.GetRpcDeliveryType()), - aznumeric_cast(entityRpcMessage.GetComponentId()), - aznumeric_cast(entityRpcMessage.GetRpcIndex()), + GetMultiplayerComponentRegistry()->GetComponentRpcName(entityRpcMessage.GetComponentId(), entityRpcMessage.GetRpcIndex()), entityRpcMessage.GetReliability() == ReliabilityType::Reliable ? "true" : "false", IsMarkedForRemoval() ? "true" : "false" ); @@ -740,23 +704,23 @@ namespace Multiplayer case RpcValidationResult::DropRpcAndDisconnect: return false; case RpcValidationResult::ForwardToClient: - { - ScopedForwardingMessage forwarding(*this); - m_netBindComponent->GetSendAuthorityToClientRpcEvent().Signal(entityRpcMessage); + { + ScopedForwardingMessage forwarding(*this); + m_netBindComponent->GetSendAuthorityToClientRpcEvent().Signal(entityRpcMessage); + } return true; - } case RpcValidationResult::ForwardToAutonomous: - { - ScopedForwardingMessage forwarding(*this); - m_netBindComponent->GetSendAuthorityToAutonomousRpcEvent().Signal(entityRpcMessage); + { + ScopedForwardingMessage forwarding(*this); + m_netBindComponent->GetSendAuthorityToAutonomousRpcEvent().Signal(entityRpcMessage); + } return true; - } case RpcValidationResult::ForwardToAuthority: - { - ScopedForwardingMessage forwarding(*this); - m_netBindComponent->GetSendServerToAuthorityRpcEvent().Signal(entityRpcMessage); + { + ScopedForwardingMessage forwarding(*this); + m_netBindComponent->GetSendServerToAuthorityRpcEvent().Signal(entityRpcMessage); + } return true; - } default: break; } diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityAuthorityTracker.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityAuthorityTracker.cpp index 0a99f4b0d4..b3f87ea9ab 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityAuthorityTracker.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityAuthorityTracker.cpp @@ -33,8 +33,8 @@ namespace Multiplayer AZLOG ( NET_AuthTracker, - "AuthTracker: Removing timeout for networkEntityId %u from %s, new owner is %s", - aznumeric_cast(entityHandle.GetNetEntityId()), + "AuthTracker: Removing timeout for networkEntityId %llu from %s, new owner is %s", + aznumeric_cast(entityHandle.GetNetEntityId()), timeoutData->second.m_previousOwner.GetString().c_str(), newOwner.GetString().c_str() ); @@ -48,8 +48,8 @@ namespace Multiplayer AZLOG ( NET_AuthTracker, - "AuthTracker: Assigning networkEntityId %u from %s to %s", - aznumeric_cast(entityHandle.GetNetEntityId()), + "AuthTracker: Assigning networkEntityId %llu from %s to %s", + aznumeric_cast(entityHandle.GetNetEntityId()), iter->second.back().GetString().c_str(), newOwner.GetString().c_str() ); @@ -59,8 +59,8 @@ namespace Multiplayer AZLOG ( NET_AuthTracker, - "AuthTracker: Assigning networkEntityId %u to %s", - aznumeric_cast(entityHandle.GetNetEntityId()), + "AuthTracker: Assigning networkEntityId %llu to %s", + aznumeric_cast(entityHandle.GetNetEntityId()), newOwner.GetString().c_str() ); } @@ -87,7 +87,7 @@ namespace Multiplayer } } - AZLOG(NET_AuthTracker, "AuthTracker: Removing networkEntityId %u from %s", aznumeric_cast(entityHandle.GetNetEntityId()), previousOwner.GetString().c_str()); + AZLOG(NET_AuthTracker, "AuthTracker: Removing networkEntityId %llu from %s", aznumeric_cast(entityHandle.GetNetEntityId()), previousOwner.GetString().c_str()); if (auto localEnt = entityHandle.GetEntity()) { if (authorityStack.empty()) @@ -114,14 +114,14 @@ namespace Multiplayer } else { - AZLOG(NET_AuthTracker, "AuthTracker: Skipping timeout for Autonomous networkEntityId %u", aznumeric_cast(entityHandle.GetNetEntityId())); + AZLOG(NET_AuthTracker, "AuthTracker: Skipping timeout for Autonomous networkEntityId %llu", aznumeric_cast(entityHandle.GetNetEntityId())); } } } } else { - AZLOG(NET_AuthTracker, "AuthTracker: Remove authority called on networkEntityId that was never added %u", aznumeric_cast(entityHandle.GetNetEntityId())); + AZLOG(NET_AuthTracker, "AuthTracker: Remove authority called on networkEntityId that was never added %llu", aznumeric_cast(entityHandle.GetNetEntityId())); AZ_Assert(false, "AuthTracker: Remove authority called on entity that was never added"); } } @@ -205,8 +205,8 @@ namespace Multiplayer { AZLOG_ERROR ( - "Timed out entity id %u during migration previous owner %s, removing it", - aznumeric_cast(entityHandle.GetNetEntityId()), + "Timed out entity id %llu during migration previous owner %s, removing it", + aznumeric_cast(entityHandle.GetNetEntityId()), timeoutData->second.m_previousOwner.GetString().c_str() ); m_networkEntityManager.MarkForRemoval(entityHandle); diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp index ef338840f9..457395a61e 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp @@ -18,22 +18,14 @@ namespace Multiplayer { ConstNetworkEntityHandle::ConstNetworkEntityHandle(AZ::Entity* entity, const NetworkEntityTracker* networkEntityTracker) : m_entity(entity) - , m_networkEntityTracker(networkEntityTracker) + , m_networkEntityTracker((networkEntityTracker != nullptr) ? networkEntityTracker : GetNetworkEntityTracker()) { - if (m_networkEntityTracker == nullptr) - { - m_networkEntityTracker = GetNetworkEntityTracker(); - } - - if (m_networkEntityTracker) - { - m_changeDirty = m_networkEntityTracker->GetChangeDirty(m_entity); - } + AZ_Assert(m_networkEntityTracker, "NetworkEntityTracker is not valid"); + m_changeDirty = m_networkEntityTracker->GetChangeDirty(m_entity); if (entity) { - AZ_Assert(networkEntityTracker, "NetworkEntityTracker is not valid"); - m_netBindComponent = networkEntityTracker->GetNetBindComponent(entity); + m_netBindComponent = m_networkEntityTracker->GetNetBindComponent(entity); if (m_netBindComponent != nullptr) { m_netEntityId = m_netBindComponent->GetNetEntityId(); diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp index db7f7243cc..c7582af83f 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include namespace Multiplayer @@ -46,6 +48,20 @@ namespace Multiplayer void NetworkEntityManager::Initialize(const HostId& hostId, AZStd::unique_ptr entityDomain) { m_hostId = hostId; + + // Configure our vended NetEntityIds so that no two hosts generate the same NetEntityId + { + // Needs more thought + const uint64_t addrPortion = hostId.GetAddress(AzNetworking::ByteOrder::Host); + const uint64_t portPortion = hostId.GetPort(AzNetworking::ByteOrder::Host); + const uint64_t hostIdentifier = (portPortion << 32) | addrPortion; + const AZ::HashValue32 hostHash = AZ::TypeHash32(hostIdentifier); + + NetEntityId hostEntityIdOffset = static_cast(hostHash) << 32; + m_nextEntityId &= NetEntityId{ 0x0000000000000000FFFFFFFFFFFFFFFF }; + m_nextEntityId |= hostEntityIdOffset; + } + m_entityDomain = AZStd::move(entityDomain); m_updateEntityDomainEvent.Enqueue(net_EntityDomainUpdateMs, true); m_entityDomain->ActivateTracking(m_ownedEntities); @@ -225,11 +241,19 @@ namespace Multiplayer { AZ::Entity* entity = it->second; NetBindComponent* netBindComponent = m_networkEntityTracker.GetNetBindComponent(entity); + AZ::Aabb entityBounds = AZ::Interface::Get()->GetEntityWorldBoundsUnion(entity->GetId()); + entityBounds.Expand(AZ::Vector3(0.01f)); if (netBindComponent->GetNetEntityRole() == NetEntityRole::Authority) { - const AZ::Aabb entityBounds = AZ::Interface::Get()->GetEntityWorldBoundsUnion(entity->GetId()); - debugDisplay->DrawWireBox(entityBounds.GetMin(), entityBounds.GetMax()); + debugDisplay->SetColor(AZ::Colors::Black); + debugDisplay->SetAlpha(0.5f); + } + else + { + debugDisplay->SetColor(AZ::Colors::DeepSkyBlue); + debugDisplay->SetAlpha(0.25f); } + debugDisplay->DrawWireBox(entityBounds.GetMin(), entityBounds.GetMax()); } if (m_entityDomain != nullptr) @@ -272,15 +296,32 @@ namespace Multiplayer bool safeToExit = true; NetworkEntityHandle entityHandle = m_networkEntityTracker.Get(entityId); - // We also need special handling for the EntityHierarchyComponent as well, since related entities need to be migrated together - //auto* hierarchyController = FindController(nonConstExitingEntityPtr); - //if (hierarchyController) - //{ - // if (hierarchyController->GetParentRelatedEntity()) - // { - // safeToExit = false; - // } - //} + // We also need special handling for the NetworkHierarchy as well, since related entities need to be migrated together + NetworkHierarchyRootComponentController* hierarchyRootController = entityHandle.FindController(); + NetworkHierarchyChildComponentController* hierarchyChildController = entityHandle.FindController(); + + // Find the root entity + AZ::Entity* hierarchyRootEntity = nullptr; + if (hierarchyRootController) + { + hierarchyRootEntity = hierarchyRootController->GetParent().GetHierarchicalRoot(); + } + else if (hierarchyChildController) + { + hierarchyRootEntity = hierarchyChildController->GetParent().GetHierarchicalRoot(); + } + + if (hierarchyRootEntity) + { + NetEntityId rootNetId = GetNetEntityIdById(hierarchyRootEntity->GetId()); + ConstNetworkEntityHandle rootEntityHandle = GetEntity(rootNetId); + + // Check if the root entity is still tracked by this authority + if (rootEntityHandle.Exists() && rootEntityHandle.GetNetBindComponent()->HasController()) + { + safeToExit = false; + } + } // Validate that we aren't already planning to remove this entity if (safeToExit) diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityUpdateMessage.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityUpdateMessage.cpp index 4a2f12ce17..d635dbaf80 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityUpdateMessage.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityUpdateMessage.cpp @@ -18,7 +18,6 @@ namespace Multiplayer , m_entityId(rhs.m_entityId) , m_isDelete(rhs.m_isDelete) , m_wasMigrated(rhs.m_wasMigrated) - , m_takeOwnership(rhs.m_takeOwnership) , m_hasValidPrefabId(rhs.m_hasValidPrefabId) , m_prefabEntityId(rhs.m_prefabEntityId) , m_data(AZStd::move(rhs.m_data)) @@ -31,7 +30,6 @@ namespace Multiplayer , m_entityId(rhs.m_entityId) , m_isDelete(rhs.m_isDelete) , m_wasMigrated(rhs.m_wasMigrated) - , m_takeOwnership(rhs.m_takeOwnership) , m_hasValidPrefabId(rhs.m_hasValidPrefabId) , m_prefabEntityId(rhs.m_prefabEntityId) { @@ -58,11 +56,10 @@ namespace Multiplayer ; } - NetworkEntityUpdateMessage::NetworkEntityUpdateMessage(NetEntityId entityId, bool wasMigrated, bool takeOwnership) + NetworkEntityUpdateMessage::NetworkEntityUpdateMessage(NetEntityId entityId, bool wasMigrated) : m_entityId(entityId) , m_isDelete(true) , m_wasMigrated(wasMigrated) - , m_takeOwnership(takeOwnership) { // this is a delete entity message c-tor } @@ -73,7 +70,6 @@ namespace Multiplayer m_entityId = rhs.m_entityId; m_isDelete = rhs.m_isDelete; m_wasMigrated = rhs.m_wasMigrated; - m_takeOwnership = rhs.m_takeOwnership; m_hasValidPrefabId = rhs.m_hasValidPrefabId; m_prefabEntityId = rhs.m_prefabEntityId; m_data = AZStd::move(rhs.m_data); @@ -86,7 +82,6 @@ namespace Multiplayer m_entityId = rhs.m_entityId; m_isDelete = rhs.m_isDelete; m_wasMigrated = rhs.m_wasMigrated; - m_takeOwnership = rhs.m_takeOwnership; m_hasValidPrefabId = rhs.m_hasValidPrefabId; m_prefabEntityId = rhs.m_prefabEntityId; if (rhs.m_data != nullptr) @@ -104,7 +99,6 @@ namespace Multiplayer && (m_entityId == rhs.m_entityId) && (m_isDelete == rhs.m_isDelete) && (m_wasMigrated == rhs.m_wasMigrated) - && (m_takeOwnership == rhs.m_takeOwnership) && (m_hasValidPrefabId == rhs.m_hasValidPrefabId) && (m_prefabEntityId == rhs.m_prefabEntityId)); } @@ -160,11 +154,6 @@ namespace Multiplayer return m_wasMigrated; } - bool NetworkEntityUpdateMessage::GetTakeOwnership() const - { - return m_takeOwnership; - } - bool NetworkEntityUpdateMessage::GetHasValidPrefabId() const { return m_hasValidPrefabId; @@ -210,17 +199,15 @@ namespace Multiplayer serializer.Serialize(m_entityId, "EntityId"); // Use the upper 4 bits for boolean flags, and the lower 4 bits for the network role - uint8_t networkTypeAndFlags = (m_isDelete ? 0x80 : 0x00) - | (m_wasMigrated ? 0x40 : 0x00) - | (m_takeOwnership ? 0x20 : 0x00) + uint8_t networkTypeAndFlags = (m_isDelete ? 0x40 : 0x00) + | (m_wasMigrated ? 0x20 : 0x00) | (m_hasValidPrefabId ? 0x10 : 0x00) | static_cast(m_networkRole); if (serializer.Serialize(networkTypeAndFlags, "TypeAndFlags")) { - m_isDelete = (networkTypeAndFlags & 0x80) == 0x80; - m_wasMigrated = (networkTypeAndFlags & 0x40) == 0x40; - m_takeOwnership = (networkTypeAndFlags & 0x20) == 0x20; + m_isDelete = (networkTypeAndFlags & 0x40) == 0x40; + m_wasMigrated = (networkTypeAndFlags & 0x20) == 0x20; m_hasValidPrefabId = (networkTypeAndFlags & 0x10) == 0x10; m_networkRole = static_cast(networkTypeAndFlags & 0x0F); } diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.cpp b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.cpp index 638dc9a900..9d566537c5 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.cpp @@ -6,7 +6,7 @@ * */ -#include +#include #include #include #include diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.cpp b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.cpp index bea110f298..59fb62e1b5 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.cpp @@ -6,7 +6,7 @@ * */ -#include +#include #include #include diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.cpp b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.cpp index 54813327ab..00fd9f13b8 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.cpp @@ -6,7 +6,7 @@ * */ -#include +#include namespace Multiplayer { diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.cpp b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.cpp index d95c46261f..ec57891725 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.cpp @@ -6,7 +6,7 @@ * */ -#include +#include #include #include diff --git a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp index e0990aa785..27e59c0bf4 100644 --- a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp +++ b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp @@ -110,7 +110,7 @@ namespace Multiplayer auto serializer = [](AZStd::vector& output, const ProcessedObjectStore& object) -> bool { AZ::IO::ByteContainerStream stream(&output); auto& asset = object.GetAsset(); - return AZ::Utils::SaveObjectToStream(stream, AZ::DataStream::ST_JSON, &asset, asset.GetType()); + return AZ::Utils::SaveObjectToStream(stream, AZ::DataStream::ST_BINARY, &asset, asset.GetType()); }; auto&& [object, networkSpawnable] = diff --git a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp index 9d9cc74d2b..deff1f640f 100644 --- a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp +++ b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -91,17 +92,10 @@ namespace Multiplayer return m_isPoorConnection ? sv_MinEntitiesToReplicate : sv_MaxEntitiesToReplicate; } - bool ServerToClientReplicationWindow::IsInWindow(const ConstNetworkEntityHandle& entityHandle, NetEntityRole& outNetworkRole) const + bool ServerToClientReplicationWindow::IsInWindow([[maybe_unused]] const ConstNetworkEntityHandle& entityHandle, NetEntityRole& outNetworkRole) const { - // TODO: Clean up this interface, this function is used for server->server migrations, and probably shouldn't be exposed in it's current setup AZ_Assert(false, "IsInWindow should not be called on the ServerToClientReplicationWindow"); outNetworkRole = NetEntityRole::InvalidRole; - auto iter = m_replicationSet.find(entityHandle); - if (iter != m_replicationSet.end()) - { - outNetworkRole = iter->second.m_netEntityRole; - return true; - } return false; } @@ -145,7 +139,7 @@ namespace Multiplayer NetworkEntityTracker* networkEntityTracker = GetNetworkEntityTracker(); IFilterEntityManager* filterEntityManager = GetMultiplayer()->GetFilterEntityManager(); - // Add all the neighbors + // Add all the neighbours for (AzFramework::VisibilityEntry* visEntry : gatheredEntries) { AZ::Entity* entity = static_cast(visEntry->m_userData); @@ -174,11 +168,11 @@ namespace Multiplayer // Note: Do not add any Client entities after this point, otherwise you stomp over the Autonomous mode m_replicationSet[m_controlledEntity] = { NetEntityRole::Autonomous, 1.0f }; // Always replicate autonomous entities - //auto hierarchyController = FindController(m_ControlledEntity); - //if (hierarchyController != nullptr) - //{ - // CollectControlledEntitiesRecursive(m_replicationSet, *hierarchyController); - //} + auto* hierarchyComponent = m_controlledEntity.FindComponent(); + if (hierarchyComponent != nullptr) + { + UpdateHierarchyReplicationSet(m_replicationSet, *hierarchyComponent); + } } AzNetworking::PacketId ServerToClientReplicationWindow::SendEntityUpdateMessages(NetworkEntityUpdateVector& entityUpdateVector) @@ -300,7 +294,6 @@ namespace Multiplayer void ServerToClientReplicationWindow::AddEntityToReplicationSet(ConstNetworkEntityHandle& entityHandle, float priority, [[maybe_unused]] float distanceSquared) { // Assumption: the entity has been checked for filtering prior to this call. - if (!sv_ReplicateServerProxies) { NetBindComponent* netBindComponent = entityHandle.GetNetBindComponent(); @@ -311,11 +304,11 @@ namespace Multiplayer } } - const bool isQueueFull = (m_candidateQueue.size() >= sv_MaxEntitiesToTrackReplication); // See if have the maximum number of entities in our set + const bool isQueueFull = (m_candidateQueue.size() >= sv_MaxEntitiesToTrackReplication); // See if have the maximum number of entities in our set const bool isInReplicationSet = m_replicationSet.find(entityHandle) != m_replicationSet.end(); if (!isInReplicationSet) { - if (isQueueFull) // if our set is full, then we need to remove the worst priority in our set + if (isQueueFull) // If our set is full, then we need to remove the worst priority in our set { ConstNetworkEntityHandle removeEnt = m_candidateQueue.top().m_entityHandle; m_candidateQueue.pop(); @@ -326,18 +319,20 @@ namespace Multiplayer } } - //void ServerToClientReplicationWindow::CollectControlledEntitiesRecursive(ReplicationSet& replicationSet, EntityHierarchyComponent::Authority& hierarchyController) - //{ - // auto controlledEnts = hierarchyController.GetChildrenRelatedEntities(); - // for (auto& controlledEnt : controlledEnts) - // { - // AZ_Assert(controlledEnt != nullptr, "We have lost a controlled entity unexpectedly"); - // replicationSet[controlledEnt.GetConstEntity()] = EntityReplicationData(EntityNetworkRoleT::e_Autonomous, EntityPrioritySystem::k_MaxPriority); // Always replicate controlled entities - // auto hierarchyController = controlledEnt.FindController(); - // if (hierarchyController != nullptr) - // { - // CollectControlledEntitiesRecursive(replicationSet, *hierarchyController); - // } - // } - //} + void ServerToClientReplicationWindow::UpdateHierarchyReplicationSet(ReplicationSet& replicationSet, NetworkHierarchyRootComponent& hierarchyComponent) + { + INetworkEntityManager* networkEntityManager = AZ::Interface::Get(); + AZ_Assert(networkEntityManager, "NetworkEntityManager must be created."); + + for (const AZ::Entity* controlledEntity : hierarchyComponent.GetHierarchicalEntities()) + { + NetEntityId controlledNetEntitydId = networkEntityManager->GetNetEntityIdById(controlledEntity->GetId()); + AZ_Assert(controlledNetEntitydId != InvalidNetEntityId, "Unable to find the hierarchy entity in Network Entity Manager"); + + ConstNetworkEntityHandle controlledEntityHandle = networkEntityManager->GetEntity(controlledNetEntitydId); + AZ_Assert(controlledEntityHandle != nullptr, "We have lost a controlled entity unexpectedly"); + + replicationSet[controlledEntityHandle] = { NetEntityRole::Autonomous, 1.0f }; + } + } } diff --git a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h index b034bde90c..3b4fddfe99 100644 --- a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h +++ b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h @@ -20,6 +20,7 @@ namespace Multiplayer { class NetSystemComponent; + class NetworkHierarchyRootComponent; class ServerToClientReplicationWindow : public IReplicationWindow @@ -56,7 +57,7 @@ namespace Multiplayer void OnEntityActivated(AZ::Entity* entity); void OnEntityDeactivated(AZ::Entity* entity); - //void CollectControlledEntitiesRecursive(ReplicationSet& replicationSet, EntityHierarchyComponent::Authority& hierarchyController); + void UpdateHierarchyReplicationSet(ReplicationSet& replicationSet, NetworkHierarchyRootComponent& hierarchyComponent); void EvaluateConnection(); void AddEntityToReplicationSet(ConstNetworkEntityHandle& entityHandle, float priority, float distanceSquared); @@ -75,8 +76,6 @@ namespace Multiplayer AZ::EntityActivatedEvent::Handler m_entityActivatedEventHandler; AZ::EntityDeactivatedEvent::Handler m_entityDeactivatedEventHandler; - //NetBindComponent* m_controlledNetBindComponent = nullptr; - AzNetworking::IConnection* m_connection = nullptr; // Cached values to detect a poor network connection diff --git a/Gems/Multiplayer/Code/Tests/AutoGen/TestMultiplayerComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Tests/AutoGen/TestMultiplayerComponent.AutoComponent.xml new file mode 100644 index 0000000000..6b18e5ac70 --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/AutoGen/TestMultiplayerComponent.AutoComponent.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp index d6dd068c3f..386496045c 100644 --- a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp +++ b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include namespace Multiplayer { @@ -175,10 +177,10 @@ namespace Multiplayer void CreateSimpleHierarchy(EntityInfo& root, EntityInfo& child) { PopulateHierarchicalEntity(root); - SetupEntity(root.m_entity, root.m_netId, NetEntityRole::Client); + SetupEntity(root.m_entity, root.m_netId, NetEntityRole::Autonomous); PopulateHierarchicalEntity(child); - SetupEntity(child.m_entity, child.m_netId, NetEntityRole::Client); + SetupEntity(child.m_entity, child.m_netId, NetEntityRole::Autonomous); // we need a parent-id value to be present in NetworkTransformComponent (which is in client mode and doesn't have a controller) SetParentIdOnNetworkTransform(child.m_entity, root.m_netId); @@ -211,9 +213,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(value), - "hierarchyRoot", /* Derived from NetworkHierarchyChildComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(value, "hierarchyRoot"); // Derived from NetworkHierarchyChildComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); @@ -301,6 +302,36 @@ namespace Multiplayer SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, InvalidNetEntityId); } + TEST_F(ClientSimpleHierarchyTests, ChildHasOwningConnectionIdOfParent) + { + // disconnect and assign new connection ids + SetParentIdOnNetworkTransform(m_child->m_entity, InvalidNetEntityId); + SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, InvalidNetEntityId); + + m_root->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 1 }); + m_child->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 2 }); + + const ConnectionId previousConnectionId = m_child->m_entity->FindComponent()->GetOwningConnectionId(); + + // re-attach, child's owning connection id should then be root's connection id + SetParentIdOnNetworkTransform(m_child->m_entity, RootNetEntityId); + SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, RootNetEntityId); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + + // detach, the child should roll back to his previous owning connection id + SetParentIdOnNetworkTransform(m_child->m_entity, InvalidNetEntityId); + SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, InvalidNetEntityId); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetOwningConnectionId(), + previousConnectionId + ); + } + /* * Parent -> Child -> ChildOfChild */ @@ -330,7 +361,7 @@ namespace Multiplayer void CreateDeepHierarchyOnClient(EntityInfo& childOfChild) { PopulateHierarchicalEntity(childOfChild); - SetupEntity(childOfChild.m_entity, childOfChild.m_netId, NetEntityRole::Client); + SetupEntity(childOfChild.m_entity, childOfChild.m_netId, NetEntityRole::Autonomous); // we need a parent-id value to be present in NetworkTransformComponent (which is in client mode and doesn't have a controller) SetParentIdOnNetworkTransform(childOfChild.m_entity, m_childOfChild->m_netId); @@ -387,4 +418,66 @@ namespace Multiplayer ); } } + + TEST_F(ClientDeepHierarchyTests, CreateProcessInputTest) + { + using MultiplayerTest::TestMultiplayerComponent; + using MultiplayerTest::TestMultiplayerComponentController; + using MultiplayerTest::TestMultiplayerComponentNetworkInput; + + auto* rootNetBind = m_root->m_entity->FindComponent(); + + NetworkInputArray inputArray(rootNetBind->GetEntityHandle()); + NetworkInput& input = inputArray[0]; + + const float deltaTime = 0.16f; + rootNetBind->CreateInput(input, deltaTime); + + auto ValidateCreatedInput = [](const NetworkInput& input, const HierarchyTests::EntityInfo& entityInfo) + { + // Validate test input for the root entity's TestMultiplayerComponent + auto* testInput = input.FindComponentInput(); + EXPECT_NE(testInput, nullptr); + + auto* testMultiplayerComponent = entityInfo.m_entity->FindComponent(); + EXPECT_NE(testMultiplayerComponent, nullptr); + + EXPECT_EQ(testInput->m_ownerId, testMultiplayerComponent->GetId()); + }; + + // Validate root input + ValidateCreatedInput(input, *m_root); + + // Validate children input + { + NetworkHierarchyRootComponentNetworkInput* rootHierarchyInput = input.FindComponentInput(); + const AZStd::vector& childInputs = rootHierarchyInput->m_childInputs; + EXPECT_EQ(childInputs.size(), 2); + ValidateCreatedInput(childInputs[0].GetNetworkInput(), *m_child); + ValidateCreatedInput(childInputs[1].GetNetworkInput(), *m_childOfChild); + } + + // Test ProcessInput + { + AZStd::unordered_set inputProcessedEntities; + size_t processInputCallCounter = 0; + auto processInputCallback = [&inputProcessedEntities, &processInputCallCounter](NetEntityId netEntityId) + { + inputProcessedEntities.insert(netEntityId); + processInputCallCounter++; + }; + + // Set the callbacks for processing input. This allows us to inspect how many times the input was processed + // and which entity's controller was invoked. + m_root->m_entity->FindComponent()->m_processInputCallback = processInputCallback; + m_child->m_entity->FindComponent()->m_processInputCallback = processInputCallback; + m_childOfChild->m_entity->FindComponent()->m_processInputCallback = processInputCallback; + + rootNetBind->ProcessInput(input, deltaTime); + + EXPECT_EQ(processInputCallCounter, 3); + EXPECT_EQ(inputProcessedEntities, + AZStd::unordered_set({ m_root->m_netId, m_child->m_netId, m_childOfChild->m_netId })); + } + } } diff --git a/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h b/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h index 5a528ed497..3c3d77e011 100644 --- a/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h +++ b/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h @@ -342,8 +342,11 @@ namespace Multiplayer void AddClientMigrationEndEventHandler([[maybe_unused]] ClientMigrationEndEvent::Handler& handler) override {} void AddNotifyClientMigrationHandler([[maybe_unused]] NotifyClientMigrationEvent::Handler& handler) override {} void AddNotifyEntityMigrationEventHandler([[maybe_unused]] NotifyEntityMigrationEvent::Handler& handler) override {} - void SendNotifyClientMigrationEvent([[maybe_unused]] const HostId& hostId, [[maybe_unused]] uint64_t userIdentifier, [[maybe_unused]] ClientInputId lastClientInputId) override {} + void SendNotifyClientMigrationEvent([[maybe_unused]] AzNetworking::ConnectionId connectionId, [[maybe_unused]] const HostId& hostId, + [[maybe_unused]] uint64_t userIdentifier, [[maybe_unused]] ClientInputId lastClientInputId, [[maybe_unused]] NetEntityId netEntityId) override {} void SendNotifyEntityMigrationEvent([[maybe_unused]] const ConstNetworkEntityHandle& entityHandle, [[maybe_unused]] const HostId& remoteHostId) override {} + void RegisterPlayerIdentifierForRejoin(uint64_t, NetEntityId) override {} + void CompleteClientMigration(uint64_t, AzNetworking::ConnectionId, const HostId&, ClientInputId) override {} void SetShouldSpawnNetworkEntities([[maybe_unused]] bool value) override {} bool GetShouldSpawnNetworkEntities() const override { return true; } @@ -535,9 +538,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(netParentId), - "parentEntityId", /* Derived from NetworkTransformComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(netParentId, "parentEntityId"); // Derived from NetworkTransformComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); @@ -560,9 +562,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(value), - "hierarchyRoot", /* Derived from NetworkHierarchyChildComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(value, "hierarchyRoot"); // Derived from NetworkHierarchyChildComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); diff --git a/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h b/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h index d6f65918c0..249837b484 100644 --- a/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h +++ b/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h @@ -30,6 +30,7 @@ #include #include #include +#include namespace Multiplayer { @@ -93,6 +94,12 @@ namespace Multiplayer m_netTransformDescriptor.reset(NetworkTransformComponent::CreateDescriptor()); m_netTransformDescriptor->Reflect(m_serializeContext.get()); + m_testMultiplayerComponentDescriptor.reset(MultiplayerTest::TestMultiplayerComponent::CreateDescriptor()); + m_testMultiplayerComponentDescriptor->Reflect(m_serializeContext.get()); + + m_testInputDriverComponentDescriptor.reset(MultiplayerTest::TestInputDriverComponent::CreateDescriptor()); + m_testInputDriverComponentDescriptor->Reflect(m_serializeContext.get()); + m_mockMultiplayer = AZStd::make_unique>(); AZ::Interface::Register(m_mockMultiplayer.get()); @@ -103,6 +110,7 @@ namespace Multiplayer GetMultiplayer()->GetStats().ReserveComponentStats(Multiplayer::InvalidNetComponentId, 50, 0); m_mockNetworkEntityManager = AZStd::make_unique>(); + AZ::Interface::Register(m_mockNetworkEntityManager.get()); ON_CALL(*m_mockNetworkEntityManager, AddEntityToEntityMap(_, _)).WillByDefault(Invoke(this, &HierarchyTests::AddEntityToEntityMap)); ON_CALL(*m_mockNetworkEntityManager, GetEntity(_)).WillByDefault(Invoke(this, &HierarchyTests::GetEntity)); @@ -136,6 +144,7 @@ namespace Multiplayer m_multiplayerComponentRegistry = AZStd::make_unique(); ON_CALL(*m_mockNetworkEntityManager, GetMultiplayerComponentRegistry()).WillByDefault(Return(m_multiplayerComponentRegistry.get())); RegisterMultiplayerComponents(); + MultiplayerTest::RegisterMultiplayerComponents(); } void TearDown() override @@ -157,6 +166,7 @@ namespace Multiplayer AZ::Interface::Unregister(m_mockNetworkTime.get()); AZ::Interface::Unregister(m_mockTime.get()); + AZ::Interface::Unregister(m_mockNetworkEntityManager.get()); AZ::Interface::Unregister(m_mockMultiplayer.get()); AZ::Interface::Unregister(m_mockComponentApplicationRequests.get()); @@ -165,6 +175,8 @@ namespace Multiplayer m_mockNetworkEntityManager.reset(); m_mockMultiplayer.reset(); + m_testInputDriverComponentDescriptor.reset(); + m_testMultiplayerComponentDescriptor.reset(); m_transformDescriptor.reset(); m_netTransformDescriptor.reset(); m_hierarchyRootDescriptor.reset(); @@ -186,6 +198,8 @@ namespace Multiplayer AZStd::unique_ptr m_hierarchyRootDescriptor; AZStd::unique_ptr m_hierarchyChildDescriptor; AZStd::unique_ptr m_netTransformDescriptor; + AZStd::unique_ptr m_testMultiplayerComponentDescriptor; + AZStd::unique_ptr m_testInputDriverComponentDescriptor; AZStd::unique_ptr> m_mockMultiplayer; AZStd::unique_ptr m_mockNetworkEntityManager; @@ -303,9 +317,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(netParentId), - "parentEntityId", /* Derived from NetworkTransformComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(netParentId, "parentEntityId"); // Derived from NetworkTransformComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); @@ -351,9 +364,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(value), - "hierarchyRoot", /* Derived from NetworkHierarchyChildComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(value, "hierarchyRoot"); // Derived from NetworkHierarchyChildComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); @@ -394,6 +406,9 @@ namespace Multiplayer entityInfo.m_entity->CreateComponent(); entityInfo.m_entity->CreateComponent(); entityInfo.m_entity->CreateComponent(); + entityInfo.m_entity->CreateComponent(); + entityInfo.m_entity->CreateComponent(); + switch (entityInfo.m_role) { case EntityInfo::Role::Root: diff --git a/Gems/Multiplayer/Code/Tests/MockInterfaces.h b/Gems/Multiplayer/Code/Tests/MockInterfaces.h index 527aeb51bc..8cebf280b9 100644 --- a/Gems/Multiplayer/Code/Tests/MockInterfaces.h +++ b/Gems/Multiplayer/Code/Tests/MockInterfaces.h @@ -33,7 +33,7 @@ namespace UnitTest MOCK_METHOD1(AddServerAcceptanceReceivedHandler, void(Multiplayer::ServerAcceptanceReceivedEvent::Handler&)); MOCK_METHOD1(AddSessionInitHandler, void(Multiplayer::SessionInitEvent::Handler&)); MOCK_METHOD1(AddSessionShutdownHandler, void(Multiplayer::SessionShutdownEvent::Handler&)); - MOCK_METHOD3(SendNotifyClientMigrationEvent, void(const Multiplayer::HostId&, uint64_t, Multiplayer::ClientInputId)); + MOCK_METHOD5(SendNotifyClientMigrationEvent, void(AzNetworking::ConnectionId, const Multiplayer::HostId&, uint64_t, Multiplayer::ClientInputId, Multiplayer::NetEntityId)); MOCK_METHOD2(SendNotifyEntityMigrationEvent, void(const Multiplayer::ConstNetworkEntityHandle&, const Multiplayer::HostId&)); MOCK_METHOD1(SendReadyForEntityUpdates, void(bool)); MOCK_CONST_METHOD0(GetCurrentHostTimeMs, AZ::TimeMs()); @@ -42,6 +42,8 @@ namespace UnitTest MOCK_METHOD0(GetNetworkEntityManager, Multiplayer::INetworkEntityManager* ()); MOCK_METHOD1(SetFilterEntityManager, void(Multiplayer::IFilterEntityManager*)); MOCK_METHOD0(GetFilterEntityManager, Multiplayer::IFilterEntityManager* ()); + MOCK_METHOD2(RegisterPlayerIdentifierForRejoin, void(uint64_t, Multiplayer::NetEntityId)); + MOCK_METHOD4(CompleteClientMigration, void(uint64_t, AzNetworking::ConnectionId, const Multiplayer::HostId&, Multiplayer::ClientInputId)); MOCK_METHOD1(SetShouldSpawnNetworkEntities, void(bool)); MOCK_CONST_METHOD0(GetShouldSpawnNetworkEntities, bool()); }; diff --git a/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp b/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp index 64381bf195..0df0fce610 100644 --- a/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp +++ b/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp @@ -17,9 +17,9 @@ #include #include #include -#include -#include -#include +#include +#include +#include namespace Multiplayer { diff --git a/Gems/Multiplayer/Code/Tests/ServerHierarchyTests.cpp b/Gems/Multiplayer/Code/Tests/ServerHierarchyTests.cpp index b8e892e71a..2888071508 100644 --- a/Gems/Multiplayer/Code/Tests/ServerHierarchyTests.cpp +++ b/Gems/Multiplayer/Code/Tests/ServerHierarchyTests.cpp @@ -195,6 +195,49 @@ namespace Multiplayer m_child->m_entity.reset(); } + TEST_F(ServerSimpleHierarchyTests, ChildPointsToRootAfterReattachment) + { + m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetHierarchyRoot(), + InvalidNetEntityId + ); + + m_child->m_entity->FindComponent()->SetParent(m_root->m_entity->GetId()); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetHierarchyRoot(), + m_root->m_entity->FindComponent()->GetNetEntityId() + ); + } + + TEST_F(ServerSimpleHierarchyTests, ChildHasOwningConnectionIdOfParent) + { + // disconnect and assign new connection ids + m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); + m_root->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 1 }); + m_child->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 2 }); + + const ConnectionId previousConnectionId = m_child->m_entity->FindComponent()->GetOwningConnectionId(); + + // re-attach, child's owning connection id should then be root's connection id + m_child->m_entity->FindComponent()->SetParent(m_root->m_entity->GetId()); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + + // detach, the child should roll back to his previous owning connection id + m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetOwningConnectionId(), + previousConnectionId + ); + } + /* * Parent -> Child -> ChildOfChild */ @@ -394,8 +437,8 @@ namespace Multiplayer m_console->PerformCommand("bg_hierarchyEntityMaxLimit 2"); // remake the hierarchy - m_root->m_entity->FindComponent()->SetParent(AZ::EntityId()); - m_root->m_entity->FindComponent()->SetParent(m_root->m_entity->GetId()); + m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); + m_child->m_entity->FindComponent()->SetParent(m_root->m_entity->GetId()); EXPECT_EQ( m_root->m_entity->FindComponent()->GetHierarchicalEntities().size(), @@ -406,6 +449,17 @@ namespace Multiplayer m_console->GetCvarValue("bg_hierarchyEntityMaxLimit", currentMaxLimit); } + TEST_F(ServerDeepHierarchyTests, ReattachMiddleChildRebuildInvokedTwice) + { + MockNetworkHierarchyCallbackHandler mock; + EXPECT_CALL(mock, OnNetworkHierarchyUpdated(m_root->m_entity->GetId())).Times(2); + + m_root->m_entity->FindComponent()->BindNetworkHierarchyChangedEventHandler(mock.m_changedHandler); + + m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); + m_child->m_entity->FindComponent()->SetParent(m_root->m_entity->GetId()); + } + /* * Parent -> Child -> Child Of Child * -> Child2 -> Child Of Child2 @@ -533,11 +587,11 @@ namespace Multiplayer ); EXPECT_EQ( m_root->m_entity->FindComponent()->GetHierarchicalEntities()[2], - m_childOfChild->m_entity.get() + m_child2->m_entity.get() ); EXPECT_EQ( m_root->m_entity->FindComponent()->GetHierarchicalEntities()[3], - m_child2->m_entity.get() + m_childOfChild->m_entity.get() ); EXPECT_EQ( m_root->m_entity->FindComponent()->GetHierarchicalEntities()[4], @@ -811,6 +865,22 @@ namespace Multiplayer } } + TEST_F(ServerHierarchyOfHierarchyTests, InnerChildrenPointToInnerRootAfterDetachmentFromTopRoot) + { + m_root2->m_entity->FindComponent()->SetParent(m_root->m_entity->GetId()); + // detach + m_root2->m_entity->FindComponent()->SetParent(AZ::EntityId()); + + EXPECT_EQ( + m_child2->m_entity->FindComponent()->GetHierarchyRoot(), + m_root2->m_entity->FindComponent()->GetNetEntityId() + ); + EXPECT_EQ( + m_childOfChild2->m_entity->FindComponent()->GetHierarchyRoot(), + m_root2->m_entity->FindComponent()->GetNetEntityId() + ); + } + TEST_F(ServerHierarchyOfHierarchyTests, Inner_Root_Has_Child_References_After_Detachment_From_Child_Of_Child) { m_root2->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); @@ -994,6 +1064,59 @@ namespace Multiplayer m_console->GetCvarValue("bg_hierarchyEntityMaxLimit", currentMaxLimit); } + TEST_F(ServerHierarchyOfHierarchyTests, InnerRootAndItsChildrenHaveOwningConnectionIdOfTopRoot) + { + // Assign new connection ids. + m_root->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 1 }); + m_root2->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 2 }); + + // Attach then inner hierarchy's owning connection id should then be top root's connection id. + m_root2->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); + + EXPECT_EQ( + m_root2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + + EXPECT_EQ( + m_child2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + + EXPECT_EQ( + m_childOfChild2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + } + + TEST_F(ServerHierarchyOfHierarchyTests, InnerRootAndItsChildrenHaveTheirOriginalOwningConnectionIdAfterDetachingFromTopRoot) + { + // Assign new connection ids. + m_root->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 1 }); + m_root2->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 2 }); + + // Attach then inner hierarchy's owning connection id should then be top root's connection id. + m_root2->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); + + // detach, inner hierarchy should roll back to his previous owning connection id + m_root2->m_entity->FindComponent()->SetParent(AZ::EntityId()); + + EXPECT_EQ( + m_root2->m_entity->FindComponent()->GetOwningConnectionId(), + ConnectionId{ 2 } + ); + + EXPECT_EQ( + m_child2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root2->m_entity->FindComponent()->GetOwningConnectionId() + ); + + EXPECT_EQ( + m_childOfChild2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root2->m_entity->FindComponent()->GetOwningConnectionId() + ); + } + /* * Parent -> Child -> ChildOfChild (not marked as in a hierarchy) */ @@ -1230,4 +1353,17 @@ namespace Multiplayer 3 ); } + + TEST_F(ServerHierarchyWithThreeRoots, InnerRootLeftTopRootThenLastChildGetsJoinedEventOnce) + { + m_root2->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); + m_root3->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); + + MockNetworkHierarchyCallbackHandler mock; + EXPECT_CALL(mock, OnNetworkHierarchyUpdated(m_root3->m_entity->GetId())); + + m_childOfChild3->m_entity->FindComponent()->BindNetworkHierarchyChangedEventHandler(mock.m_changedHandler); + + m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); + } } diff --git a/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.cpp b/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.cpp new file mode 100644 index 0000000000..ac723300a9 --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.cpp @@ -0,0 +1,76 @@ +/* +* 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 MultiplayerTest +{ + void TestInputDriverComponent::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class() + ->Version(1); + } + } + + void TestMultiplayerComponent::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class() + ->Version(1); + } + TestMultiplayerComponentBase::Reflect(context); + } + + void TestMultiplayerComponent::OnInit() + { + } + + void TestMultiplayerComponent::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + } + + void TestMultiplayerComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + } + + TestMultiplayerComponentController::TestMultiplayerComponentController(TestMultiplayerComponent& parent) + : TestMultiplayerComponentControllerBase(parent) + { + } + + void TestMultiplayerComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + } + + void TestMultiplayerComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + } + + void TestMultiplayerComponentController::CreateInput(Multiplayer::NetworkInput& input, [[maybe_unused]] float deltaTime) + { + auto* networkInput = input.FindComponentInput(); + networkInput->m_ownerId = GetParent().GetId(); + } + + void TestMultiplayerComponentController::ProcessInput(Multiplayer::NetworkInput& input, [[maybe_unused]] float deltaTime) + { + auto& component = GetParent(); + [[maybe_unused]] auto* networkInput = input.FindComponentInput(); + AZ_Assert(networkInput->m_ownerId == component.GetId(), "Input Id doesn't match the owner component Id"); + + if (component.m_processInputCallback) + { + component.m_processInputCallback(GetNetEntityId()); + } + } +} diff --git a/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.h b/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.h new file mode 100644 index 0000000000..4cf0be2c2d --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.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 + +namespace MultiplayerTest +{ + // Dummy class for satisfying "MultiplayerInputDriver" component dependency + class TestInputDriverComponent : public AZ::Component + { + public: + AZ_COMPONENT(TestInputDriverComponent, "{C3877905-3B61-45AE-A636-9845C3AAA39D}"); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.emplace_back(AZ_CRC_CE("MultiplayerInputDriver")); + } + + void Activate() override {} + void Deactivate() override {} + }; + + // Test multiplayer component with ability to create and process network input + class TestMultiplayerComponent + : public TestMultiplayerComponentBase + { + public: + AZ_MULTIPLAYER_COMPONENT(MultiplayerTest::TestMultiplayerComponent, s_testMultiplayerComponentConcreteUuid, MultiplayerTest::TestMultiplayerComponentBase); + + static void Reflect(AZ::ReflectContext* context); + + void OnInit() override; + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + AZStd::function m_processInputCallback; + }; + + // Multiplayer controller for the test component + class TestMultiplayerComponentController + : public TestMultiplayerComponentControllerBase + { + public: + TestMultiplayerComponentController(TestMultiplayerComponent& parent); + + //! TestMultiplayerComponentControllerBase + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + //! MultiplayerController interface + void CreateInput(Multiplayer::NetworkInput& input, float deltaTime) override; + void ProcessInput(Multiplayer::NetworkInput& input, float deltaTime) override; + }; +} diff --git a/Gems/Multiplayer/Code/multiplayer_files.cmake b/Gems/Multiplayer/Code/multiplayer_files.cmake index 292fba5624..1376083443 100644 --- a/Gems/Multiplayer/Code/multiplayer_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_files.cmake @@ -46,6 +46,10 @@ set(FILES Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h Include/Multiplayer/NetworkInput/IMultiplayerComponentInput.h Include/Multiplayer/NetworkInput/NetworkInput.h + Include/Multiplayer/NetworkInput/NetworkInputArray.h + Include/Multiplayer/NetworkInput/NetworkInputChild.h + Include/Multiplayer/NetworkInput/NetworkInputHistory.h + Include/Multiplayer/NetworkInput/NetworkInputMigrationVector.h Include/Multiplayer/NetworkTime/INetworkTime.h Include/Multiplayer/NetworkTime/RewindableArray.h Include/Multiplayer/NetworkTime/RewindableArray.inl @@ -115,13 +119,9 @@ set(FILES Source/NetworkEntity/NetworkSpawnableLibrary.h Source/NetworkInput/NetworkInput.cpp Source/NetworkInput/NetworkInputArray.cpp - Source/NetworkInput/NetworkInputArray.h Source/NetworkInput/NetworkInputChild.cpp - Source/NetworkInput/NetworkInputChild.h Source/NetworkInput/NetworkInputHistory.cpp - Source/NetworkInput/NetworkInputHistory.h Source/NetworkInput/NetworkInputMigrationVector.cpp - Source/NetworkInput/NetworkInputMigrationVector.h Source/NetworkTime/NetworkTime.cpp Source/NetworkTime/NetworkTime.h Source/Pipeline/NetworkSpawnableHolderComponent.cpp diff --git a/Gems/Multiplayer/Code/multiplayer_tests_files.cmake b/Gems/Multiplayer/Code/multiplayer_tests_files.cmake index 12c92ef5c1..adc391fe31 100644 --- a/Gems/Multiplayer/Code/multiplayer_tests_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_tests_files.cmake @@ -7,6 +7,12 @@ # set(FILES + Include/Multiplayer/AutoGen/AutoComponentTypes_Header.jinja + Include/Multiplayer/AutoGen/AutoComponentTypes_Source.jinja + Include/Multiplayer/AutoGen/AutoComponent_Common.jinja + Include/Multiplayer/AutoGen/AutoComponent_Header.jinja + Include/Multiplayer/AutoGen/AutoComponent_Source.jinja + Tests/AutoGen/TestMultiplayerComponent.AutoComponent.xml Tests/ClientHierarchyTests.cpp Tests/ServerHierarchyBenchmarks.cpp Tests/CommonHierarchySetup.h @@ -20,4 +26,6 @@ set(FILES Tests/RewindableContainerTests.cpp Tests/RewindableObjectTests.cpp Tests/ServerHierarchyTests.cpp + Tests/TestMultiplayerComponent.h + Tests/TestMultiplayerComponent.cpp ) diff --git a/Gems/PhysX/Code/Editor/DebugDraw.cpp b/Gems/PhysX/Code/Editor/DebugDraw.cpp index 5936312ecf..90a1eb6004 100644 --- a/Gems/PhysX/Code/Editor/DebugDraw.cpp +++ b/Gems/PhysX/Code/Editor/DebugDraw.cpp @@ -264,7 +264,11 @@ namespace PhysX case Physics::ShapeType::CookedMesh: { const auto& cookedMeshConfig = static_cast(shapeConfig); - physx::PxBase* meshData = static_cast(cookedMeshConfig.GetCachedNativeMesh()); + const physx::PxBase* constMeshData = static_cast(cookedMeshConfig.GetCachedNativeMesh()); + + // Specifically removing the const from the meshData pointer because the physx APIs expect this pointer to be non-const. + physx::PxBase* meshData = const_cast(constMeshData); + if (meshData) { if (meshData->is()) @@ -676,7 +680,17 @@ namespace PhysX } } - AZ::Transform Collider::GetColliderLocalTransform(const Physics::ColliderConfiguration& colliderConfig, + void Collider::DrawHeightfield( + [[maybe_unused]] AzFramework::DebugDisplayRequests& debugDisplay, + [[maybe_unused]] const Physics::ColliderConfiguration& colliderConfig, + [[maybe_unused]] const Physics::HeightfieldShapeConfiguration& heightfieldShapeConfig, + [[maybe_unused]] const AZ::Vector3& colliderScale, + [[maybe_unused]] const bool forceUniformScaling) const + { + } + + AZ::Transform Collider::GetColliderLocalTransform( + const Physics::ColliderConfiguration& colliderConfig, const AZ::Vector3& colliderScale) const { // Apply entity world transform scale to collider offset diff --git a/Gems/PhysX/Code/Editor/DebugDraw.h b/Gems/PhysX/Code/Editor/DebugDraw.h index 774def6900..74f08aae99 100644 --- a/Gems/PhysX/Code/Editor/DebugDraw.h +++ b/Gems/PhysX/Code/Editor/DebugDraw.h @@ -104,7 +104,15 @@ namespace PhysX const AZ::Vector3& meshScale, AZ::u32 geomIndex) const; - void DrawPolygonPrism(AzFramework::DebugDisplayRequests& debugDisplay, + void DrawHeightfield( + AzFramework::DebugDisplayRequests& debugDisplay, + const Physics::ColliderConfiguration& colliderConfig, + const Physics::HeightfieldShapeConfiguration& heightfieldShapeConfig, + const AZ::Vector3& colliderScale = AZ::Vector3::CreateOne(), + const bool forceUniformScaling = false) const; + + void DrawPolygonPrism( + AzFramework::DebugDisplayRequests& debugDisplay, const Physics::ColliderConfiguration& colliderConfig, const AZStd::vector& points) const; AZ::Transform GetColliderLocalTransform(const Physics::ColliderConfiguration& colliderConfig, diff --git a/Gems/PhysX/Code/Include/PhysX/SystemComponentBus.h b/Gems/PhysX/Code/Include/PhysX/SystemComponentBus.h index f9bd2dbad5..a31a4e65b8 100644 --- a/Gems/PhysX/Code/Include/PhysX/SystemComponentBus.h +++ b/Gems/PhysX/Code/Include/PhysX/SystemComponentBus.h @@ -16,19 +16,21 @@ namespace AzPhysics { class CollisionGroup; class CollisionLayer; -} +} // namespace AzPhysics namespace physx { class PxScene; class PxSceneDesc; class PxConvexMesh; + class PxHeightField; class PxTriangleMesh; class PxShape; class PxCooking; class PxControllerManager; struct PxFilterData; -} + struct PxHeightFieldSample; +} // namespace physx namespace PhysX { @@ -63,6 +65,13 @@ namespace PhysX /// @return Pointer to the created mesh. virtual physx::PxTriangleMesh* CreateTriangleMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) = 0; + /// Creates a new heightfield. + /// @param samples Pointer to beginning of heightfield sample data. + /// @param numRows Number of rows in the heightfield. + /// @param numColumns Number of columns in the heightfield. + /// @return Pointer to the created heightfield. + virtual physx::PxHeightField* CreateHeightField(const physx::PxHeightFieldSample* samples, AZ::u32 numRows, AZ::u32 numColumns) = 0; + /// Creates PhysX collision filter data from generic collision filtering settings. /// @param layer The collision layer the object belongs to. /// @param group The set of collision layers the object will interact with. diff --git a/Gems/PhysX/Code/Source/ComponentDescriptors.cpp b/Gems/PhysX/Code/Source/ComponentDescriptors.cpp index ebe6efd310..83232edc40 100644 --- a/Gems/PhysX/Code/Source/ComponentDescriptors.cpp +++ b/Gems/PhysX/Code/Source/ComponentDescriptors.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ namespace PhysX BaseColliderComponent::CreateDescriptor(), MeshColliderComponent::CreateDescriptor(), BoxColliderComponent::CreateDescriptor(), + HeightfieldColliderComponent::CreateDescriptor(), SphereColliderComponent::CreateDescriptor(), CapsuleColliderComponent::CreateDescriptor(), ShapeColliderComponent::CreateDescriptor(), diff --git a/Gems/PhysX/Code/Source/EditorComponentDescriptors.cpp b/Gems/PhysX/Code/Source/EditorComponentDescriptors.cpp index d43d0edc23..362903f638 100644 --- a/Gems/PhysX/Code/Source/EditorComponentDescriptors.cpp +++ b/Gems/PhysX/Code/Source/EditorComponentDescriptors.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ namespace PhysX EditorColliderComponent::CreateDescriptor(), EditorFixedJointComponent::CreateDescriptor(), EditorForceRegionComponent::CreateDescriptor(), + EditorHeightfieldColliderComponent::CreateDescriptor(), EditorHingeJointComponent::CreateDescriptor(), EditorJointComponent::CreateDescriptor(), EditorRigidBodyComponent::CreateDescriptor(), diff --git a/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp b/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp new file mode 100644 index 0000000000..0f09258524 --- /dev/null +++ b/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp @@ -0,0 +1,339 @@ +/* + * 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 PhysX +{ + void EditorHeightfieldColliderComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("ColliderConfiguration", &EditorHeightfieldColliderComponent::m_colliderConfig) + ->Field("DebugDrawSettings", &EditorHeightfieldColliderComponent::m_colliderDebugDraw) + ->Field("ShapeConfig", &EditorHeightfieldColliderComponent::m_shapeConfig) + ; + + if (auto editContext = serializeContext->GetEditContext()) + { + editContext->Class( + "PhysX Heightfield Collider", "Creates geometry in the PhysX simulation based on an attached heightfield component") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Category, "PhysX") + ->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/PhysXCollider.svg") + ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/Viewport/PhysXCollider.svg") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Game")) + ->Attribute( + AZ::Edit::Attributes::HelpPageURL, "https://o3de.org/docs/user-guide/components/reference/physx/heightfield-collider/") + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->DataElement( + AZ::Edit::UIHandlers::Default, &EditorHeightfieldColliderComponent::m_colliderConfig, "Collider configuration", + "Configuration of the collider") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorHeightfieldColliderComponent::OnConfigurationChanged) + ->DataElement( + AZ::Edit::UIHandlers::Default, &EditorHeightfieldColliderComponent::m_colliderDebugDraw, "Debug draw settings", + "Debug draw settings") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ; + } + } + } + + void EditorHeightfieldColliderComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("PhysicsWorldBodyService")); + provided.push_back(AZ_CRC_CE("PhysXColliderService")); + provided.push_back(AZ_CRC_CE("PhysXHeightfieldColliderService")); + } + + void EditorHeightfieldColliderComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + { + required.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService")); + } + + void EditorHeightfieldColliderComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("PhysXColliderService")); + incompatible.push_back(AZ_CRC_CE("PhysXStaticRigidBodyService")); + incompatible.push_back(AZ_CRC_CE("PhysXRigidBodyService")); + } + + EditorHeightfieldColliderComponent::EditorHeightfieldColliderComponent() + : m_physXConfigChangedHandler( + []([[maybe_unused]] const AzPhysics::SystemConfiguration* config) + { + AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast( + &AzToolsFramework::PropertyEditorGUIMessages::RequestRefresh, + AzToolsFramework::PropertyModificationRefreshLevel::Refresh_AttributesAndValues); + }) + , m_onMaterialLibraryChangedEventHandler( + [this](const AZ::Data::AssetId& defaultMaterialLibrary) + { + m_colliderConfig.m_materialSelection.OnMaterialLibraryChanged(defaultMaterialLibrary); + Physics::ColliderComponentEventBus::Event(GetEntityId(), &Physics::ColliderComponentEvents::OnColliderChanged); + + AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast( + &AzToolsFramework::PropertyEditorGUIMessages::RequestRefresh, + AzToolsFramework::PropertyModificationRefreshLevel::Refresh_AttributesAndValues); + }) + { + } + + EditorHeightfieldColliderComponent ::~EditorHeightfieldColliderComponent() + { + ClearHeightfield(); + } + + // AZ::Component + void EditorHeightfieldColliderComponent::Activate() + { + AzToolsFramework::Components::EditorComponentBase::Activate(); + + // Heightfields don't support the following: + // - Offset: There shouldn't be a need to offset the data, since the heightfield provider is giving a physics representation + // - IsTrigger: PhysX heightfields don't support acting as triggers + // - MaterialSelection: The heightfield provider provides per-vertex material selection + m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::Offset, false); + m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::IsTrigger, false); + m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::MaterialSelection, false); + + m_sceneInterface = AZ::Interface::Get(); + if (m_sceneInterface) + { + m_attachedSceneHandle = m_sceneInterface->GetSceneHandle(AzPhysics::EditorPhysicsSceneName); + } + + const AZ::EntityId entityId = GetEntityId(); + + AzToolsFramework::EntitySelectionEvents::Bus::Handler::BusConnect(entityId); + + // Debug drawing + m_colliderDebugDraw.Connect(entityId); + m_colliderDebugDraw.SetDisplayCallback(this); + + Physics::HeightfieldProviderNotificationBus::Handler::BusConnect(entityId); + PhysX::ColliderShapeRequestBus::Handler::BusConnect(entityId); + AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusConnect(entityId); + + RefreshHeightfield(); + } + + void EditorHeightfieldColliderComponent::Deactivate() + { + AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusDisconnect(); + PhysX::ColliderShapeRequestBus::Handler::BusDisconnect(); + Physics::HeightfieldProviderNotificationBus::Handler::BusDisconnect(); + + m_colliderDebugDraw.Disconnect(); + AzToolsFramework::EntitySelectionEvents::Bus::Handler::BusDisconnect(); + AzToolsFramework::Components::EditorComponentBase::Deactivate(); + + ClearHeightfield(); + } + + void EditorHeightfieldColliderComponent::BuildGameEntity(AZ::Entity* gameEntity) + { + auto* heightfieldColliderComponent = gameEntity->CreateComponent(); + heightfieldColliderComponent->SetShapeConfiguration( + { AZStd::make_shared(m_colliderConfig), m_shapeConfig }); + } + + void EditorHeightfieldColliderComponent::OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) + { + RefreshHeightfield(); + } + + void EditorHeightfieldColliderComponent::ClearHeightfield() + { + // There are two references to the heightfield data, we need to clear both to make the heightfield clear out and deallocate: + // - The simulated body has a pointer to the shape, which has a GeometryHolder, which has the Heightfield inside it + // - The shape config is also holding onto a pointer to the Heightfield + + // We remove the simulated body first, since we don't want the heightfield to exist any more. + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + m_sceneInterface->RemoveSimulatedBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + + // Now we can safely clear out the cached heightfield pointer. + m_shapeConfig->SetCachedNativeHeightfield(nullptr); + } + + void EditorHeightfieldColliderComponent::InitStaticRigidBody() + { + // Get the transform from the HeightfieldProvider. Because rotation and scale can indirectly affect how the heightfield itself + // is computed and the size of the heightfield, it's possible that the HeightfieldProvider will provide a different transform + // back to us than the one that's directly on that entity. + AZ::Transform transform = AZ::Transform::CreateIdentity(); + Physics::HeightfieldProviderRequestsBus::EventResult( + transform, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldTransform); + + AzPhysics::StaticRigidBodyConfiguration configuration; + configuration.m_orientation = transform.GetRotation(); + configuration.m_position = transform.GetTranslation(); + configuration.m_entityId = GetEntityId(); + configuration.m_debugName = GetEntity()->GetName(); + + AzPhysics::ShapeColliderPairList colliderShapePairs; + colliderShapePairs.emplace_back(AZStd::make_shared(m_colliderConfig), m_shapeConfig); + configuration.m_colliderAndShapeData = colliderShapePairs; + + if (m_sceneInterface) + { + m_staticRigidBodyHandle = m_sceneInterface->AddSimulatedBody(m_attachedSceneHandle, &configuration); + } + } + + void EditorHeightfieldColliderComponent::InitHeightfieldShapeConfiguration() + { + *m_shapeConfig = Utils::CreateHeightfieldShapeConfiguration(GetEntityId()); + } + + void EditorHeightfieldColliderComponent::RefreshHeightfield() + { + ClearHeightfield(); + InitHeightfieldShapeConfiguration(); + InitStaticRigidBody(); + Physics::ColliderComponentEventBus::Event(GetEntityId(), &Physics::ColliderComponentEvents::OnColliderChanged); + } + + AZ::u32 EditorHeightfieldColliderComponent::OnConfigurationChanged() + { + RefreshHeightfield(); + return AZ::Edit::PropertyRefreshLevels::None; + } + + // AzToolsFramework::EntitySelectionEvents + void EditorHeightfieldColliderComponent::OnSelected() + { + if (auto* physXSystem = GetPhysXSystem()) + { + if (!m_physXConfigChangedHandler.IsConnected()) + { + physXSystem->RegisterSystemConfigurationChangedEvent(m_physXConfigChangedHandler); + } + if (!m_onMaterialLibraryChangedEventHandler.IsConnected()) + { + physXSystem->RegisterOnMaterialLibraryChangedEventHandler(m_onMaterialLibraryChangedEventHandler); + } + } + } + + // AzToolsFramework::EntitySelectionEvents + void EditorHeightfieldColliderComponent::OnDeselected() + { + m_onMaterialLibraryChangedEventHandler.Disconnect(); + m_physXConfigChangedHandler.Disconnect(); + } + + // DisplayCallback + void EditorHeightfieldColliderComponent::Display(AzFramework::DebugDisplayRequests& debugDisplay) const + { + const auto& heightfieldConfig = static_cast(*m_shapeConfig); + m_colliderDebugDraw.DrawHeightfield(debugDisplay, m_colliderConfig, heightfieldConfig); + } + + // SimulatedBodyComponentRequestsBus + void EditorHeightfieldColliderComponent::EnablePhysics() + { + if (!IsPhysicsEnabled() && m_sceneInterface) + { + m_sceneInterface->EnableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + } + + // SimulatedBodyComponentRequestsBus + void EditorHeightfieldColliderComponent::DisablePhysics() + { + if (m_sceneInterface) + { + m_sceneInterface->DisableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + } + + // SimulatedBodyComponentRequestsBus + bool EditorHeightfieldColliderComponent::IsPhysicsEnabled() const + { + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->m_simulating; + } + } + return false; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SimulatedBodyHandle EditorHeightfieldColliderComponent::GetSimulatedBodyHandle() const + { + return m_staticRigidBodyHandle; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SimulatedBody* EditorHeightfieldColliderComponent::GetSimulatedBody() + { + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body; + } + } + return nullptr; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SceneQueryHit EditorHeightfieldColliderComponent::RayCast(const AzPhysics::RayCastRequest& request) + { + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->RayCast(request); + } + } + return AzPhysics::SceneQueryHit(); + } + + // ColliderShapeRequestBus + AZ::Aabb EditorHeightfieldColliderComponent::GetColliderShapeAabb() + { + // Get the Collider AABB directly from the heightfield provider. + AZ::Aabb colliderAabb = AZ::Aabb::CreateNull(); + Physics::HeightfieldProviderRequestsBus::EventResult( + colliderAabb, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldAabb); + + return colliderAabb; + } + + // SimulatedBodyComponentRequestsBus + AZ::Aabb EditorHeightfieldColliderComponent::GetAabb() const + { + // On the SimulatedBodyComponentRequestsBus, get the AABB from the simulated body instead of the collider. + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->GetAabb(); + } + } + return AZ::Aabb::CreateNull(); + } + +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.h b/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.h new file mode 100644 index 0000000000..08b3ec5801 --- /dev/null +++ b/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.h @@ -0,0 +1,104 @@ +/* + * 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 +#include +#include + +#include + +namespace PhysX +{ + //! Editor PhysX Heightfield Collider Component. + class EditorHeightfieldColliderComponent + : public AzToolsFramework::Components::EditorComponentBase + , protected AzToolsFramework::EntitySelectionEvents::Bus::Handler + , protected DebugDraw::DisplayCallback + , protected AzPhysics::SimulatedBodyComponentRequestsBus::Handler + , protected PhysX::ColliderShapeRequestBus::Handler + , protected Physics::HeightfieldProviderNotificationBus::Handler + { + public: + AZ_EDITOR_COMPONENT( + EditorHeightfieldColliderComponent, + "{C388C3DB-8D2E-4D26-96D3-198EDC799B77}", + AzToolsFramework::Components::EditorComponentBase); + static void Reflect(AZ::ReflectContext* context); + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + + EditorHeightfieldColliderComponent(); + ~EditorHeightfieldColliderComponent(); + + // AZ::Component + void Activate() override; + void Deactivate() override; + + // EditorComponentBase + void BuildGameEntity(AZ::Entity* gameEntity) override; + + protected: + + // AzToolsFramework::EntitySelectionEvents + void OnSelected() override; + void OnDeselected() override; + + // DisplayCallback + void Display(AzFramework::DebugDisplayRequests& debugDisplay) const; + + // ColliderShapeRequestBus + AZ::Aabb GetColliderShapeAabb() override; + bool IsTrigger() override + { + // PhysX Heightfields don't support triggers. + return false; + } + + // AzPhysics::SimulatedBodyComponentRequestsBus::Handler overrides ... + void EnablePhysics() override; + void DisablePhysics() override; + bool IsPhysicsEnabled() const override; + AZ::Aabb GetAabb() const override; + AzPhysics::SimulatedBody* GetSimulatedBody() override; + AzPhysics::SimulatedBodyHandle GetSimulatedBodyHandle() const override; + AzPhysics::SceneQueryHit RayCast(const AzPhysics::RayCastRequest& request) override; + + // Physics::HeightfieldProviderNotificationBus + void OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) override; + + private: + AZ::u32 OnConfigurationChanged(); + + void ClearHeightfield(); + void InitHeightfieldShapeConfiguration(); + void InitStaticRigidBody(); + void RefreshHeightfield(); + + DebugDraw::Collider m_colliderDebugDraw; //!< Handles drawing the collider + AzPhysics::SceneInterface* m_sceneInterface{ nullptr }; + + AzPhysics::SystemEvents::OnConfigurationChangedEvent::Handler m_physXConfigChangedHandler; + AzPhysics::SystemEvents::OnMaterialLibraryChangedEvent::Handler m_onMaterialLibraryChangedEventHandler; + + Physics::ColliderConfiguration m_colliderConfig; //!< Stores collision layers, whether the collider is a trigger, etc. + AZStd::shared_ptr m_shapeConfig{ new Physics::HeightfieldShapeConfiguration() }; + + AzPhysics::SimulatedBodyHandle m_staticRigidBodyHandle = + AzPhysics::InvalidSimulatedBodyHandle; //!< Handle to the body in the editor physics scene if there is no rigid body component. + AzPhysics::SceneHandle m_attachedSceneHandle = AzPhysics::InvalidSceneHandle; + }; + +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp b/Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp new file mode 100644 index 0000000000..c1fa09f21f --- /dev/null +++ b/Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp @@ -0,0 +1,365 @@ +/* + * 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 +#include +#include + +namespace PhysX +{ + void HeightfieldColliderComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("ShapeConfig", &HeightfieldColliderComponent::m_shapeConfig) + ; + } + } + + void HeightfieldColliderComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("PhysicsWorldBodyService")); + provided.push_back(AZ_CRC_CE("PhysXColliderService")); + provided.push_back(AZ_CRC_CE("PhysXHeightfieldColliderService")); + provided.push_back(AZ_CRC_CE("PhysXStaticRigidBodyService")); + } + + void HeightfieldColliderComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + { + required.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService")); + } + + void HeightfieldColliderComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("PhysXColliderService")); + incompatible.push_back(AZ_CRC_CE("PhysXStaticRigidBodyService")); + incompatible.push_back(AZ_CRC_CE("PhysXRigidBodyService")); + } + + HeightfieldColliderComponent::~HeightfieldColliderComponent() + { + ClearHeightfield(); + } + + void HeightfieldColliderComponent::Activate() + { + const AZ::EntityId entityId = GetEntityId(); + + Physics::HeightfieldProviderNotificationBus::Handler::BusConnect(entityId); + ColliderComponentRequestBus::Handler::BusConnect(entityId); + ColliderShapeRequestBus::Handler::BusConnect(entityId); + Physics::CollisionFilteringRequestBus::Handler::BusConnect(entityId); + AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusConnect(entityId); + + RefreshHeightfield(); + } + + void HeightfieldColliderComponent::Deactivate() + { + AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusDisconnect(); + Physics::CollisionFilteringRequestBus::Handler::BusDisconnect(); + ColliderShapeRequestBus::Handler::BusDisconnect(); + ColliderComponentRequestBus::Handler::BusDisconnect(); + Physics::HeightfieldProviderNotificationBus::Handler::BusDisconnect(); + + ClearHeightfield(); + } + + void HeightfieldColliderComponent::OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) + { + RefreshHeightfield(); + } + + void HeightfieldColliderComponent::ClearHeightfield() + { + // There are two references to the heightfield data, we need to clear both to make the heightfield clear out and deallocate: + // - The simulated body has a pointer to the shape, which has a GeometryHolder, which has the Heightfield inside it + // - The shape config is also holding onto a pointer to the Heightfield + + // We remove the simulated body first, since we don't want the heightfield to exist any more. + if (auto* sceneInterface = AZ::Interface::Get(); + sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + sceneInterface->RemoveSimulatedBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + + // Now we can safely clear out the cached heightfield pointer. + Physics::HeightfieldShapeConfiguration& configuration = static_cast(*m_shapeConfig.second); + configuration.SetCachedNativeHeightfield(nullptr); + } + + void HeightfieldColliderComponent::InitStaticRigidBody() + { + // Get the transform from the HeightfieldProvider. Because rotation and scale can indirectly affect how the heightfield itself + // is computed and the size of the heightfield, it's possible that the HeightfieldProvider will provide a different transform + // back to us than the one that's directly on that entity. + AZ::Transform transform = AZ::Transform::CreateIdentity(); + Physics::HeightfieldProviderRequestsBus::EventResult( + transform, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldTransform); + + AzPhysics::StaticRigidBodyConfiguration configuration; + configuration.m_orientation = transform.GetRotation(); + configuration.m_position = transform.GetTranslation(); + configuration.m_entityId = GetEntityId(); + configuration.m_debugName = GetEntity()->GetName(); + configuration.m_colliderAndShapeData = GetShapeConfigurations(); + + if (m_attachedSceneHandle == AzPhysics::InvalidSceneHandle) + { + Physics::DefaultWorldBus::BroadcastResult(m_attachedSceneHandle, &Physics::DefaultWorldRequests::GetDefaultSceneHandle); + } + if (auto* sceneInterface = AZ::Interface::Get()) + { + m_staticRigidBodyHandle = sceneInterface->AddSimulatedBody(m_attachedSceneHandle, &configuration); + } + } + + void HeightfieldColliderComponent::InitHeightfieldShapeConfiguration() + { + Physics::HeightfieldShapeConfiguration& configuration = static_cast(*m_shapeConfig.second); + + configuration = Utils::CreateHeightfieldShapeConfiguration(GetEntityId()); + } + + void HeightfieldColliderComponent::RefreshHeightfield() + { + ClearHeightfield(); + InitHeightfieldShapeConfiguration(); + InitStaticRigidBody(); + Physics::ColliderComponentEventBus::Event(GetEntityId(), &Physics::ColliderComponentEvents::OnColliderChanged); + } + + void HeightfieldColliderComponent::SetShapeConfiguration(const AzPhysics::ShapeColliderPair& shapeConfig) + { + if (GetEntity()->GetState() == AZ::Entity::State::Active) + { + AZ_Warning( + "PhysX", false, "Trying to call SetShapeConfiguration for entity \"%s\" while entity is active.", + GetEntity()->GetName().c_str()); + return; + } + m_shapeConfig = shapeConfig; + } + + // SimulatedBodyComponentRequestsBus + void HeightfieldColliderComponent::EnablePhysics() + { + if (IsPhysicsEnabled()) + { + return; + } + if (auto* sceneInterface = AZ::Interface::Get()) + { + sceneInterface->EnableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + } + + // SimulatedBodyComponentRequestsBus + void HeightfieldColliderComponent::DisablePhysics() + { + if (auto* sceneInterface = AZ::Interface::Get()) + { + sceneInterface->DisableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + } + + // SimulatedBodyComponentRequestsBus + bool HeightfieldColliderComponent::IsPhysicsEnabled() const + { + if (m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* sceneInterface = AZ::Interface::Get(); + sceneInterface != nullptr && sceneInterface->IsEnabled(m_attachedSceneHandle)) // check if the scene is enabled + { + if (AzPhysics::SimulatedBody* body = + sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->m_simulating; + } + } + } + return false; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SimulatedBodyHandle HeightfieldColliderComponent::GetSimulatedBodyHandle() const + { + return m_staticRigidBodyHandle; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SimulatedBody* HeightfieldColliderComponent::GetSimulatedBody() + { + if (auto* sceneInterface = AZ::Interface::Get()) + { + return sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + return nullptr; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SceneQueryHit HeightfieldColliderComponent::RayCast(const AzPhysics::RayCastRequest& request) + { + if (auto* body = azdynamic_cast(GetSimulatedBody())) + { + return body->RayCast(request); + } + return AzPhysics::SceneQueryHit(); + } + + // ColliderComponentRequestBus + AzPhysics::ShapeColliderPairList HeightfieldColliderComponent::GetShapeConfigurations() + { + AzPhysics::ShapeColliderPairList shapeConfigurationList({ m_shapeConfig }); + return shapeConfigurationList; + } + + AZStd::shared_ptr HeightfieldColliderComponent::GetHeightfieldShape() + { + if (auto* body = azdynamic_cast(GetSimulatedBody())) + { + // Heightfields should only have one shape + AZ_Assert(body->GetShapeCount() == 1, "Heightfield rigid body has the wrong number of shapes: %zu", body->GetShapeCount()); + return body->GetShape(0); + } + + return {}; + } + + // ColliderComponentRequestBus + AZStd::vector> HeightfieldColliderComponent::GetShapes() + { + return { GetHeightfieldShape() }; + } + + // PhysX::ColliderShapeBus + AZ::Aabb HeightfieldColliderComponent::GetColliderShapeAabb() + { + // Get the Collider AABB directly from the heightfield provider. + AZ::Aabb colliderAabb = AZ::Aabb::CreateNull(); + Physics::HeightfieldProviderRequestsBus::EventResult( + colliderAabb, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldAabb); + + return colliderAabb; + } + + // SimulatedBodyComponentRequestsBus + AZ::Aabb HeightfieldColliderComponent::GetAabb() const + { + // On the SimulatedBodyComponentRequestsBus, get the AABB from the simulated body instead of the collider. + if (auto* sceneInterface = AZ::Interface::Get()) + { + if (AzPhysics::SimulatedBody* body = sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->GetAabb(); + } + } + return AZ::Aabb::CreateNull(); + } + + // CollisionFilteringRequestBus + void HeightfieldColliderComponent::SetCollisionLayer(const AZStd::string& layerName, AZ::Crc32 colliderTag) + { + if (auto heightfield = GetHeightfieldShape()) + { + if (Physics::Utils::FilterTag(heightfield->GetTag(), colliderTag)) + { + bool success = false; + AzPhysics::CollisionLayer layer; + Physics::CollisionRequestBus::BroadcastResult( + success, &Physics::CollisionRequests::TryGetCollisionLayerByName, layerName, layer); + if (success) + { + heightfield->SetCollisionLayer(layer); + } + } + } + } + + // CollisionFilteringRequestBus + AZStd::string HeightfieldColliderComponent::GetCollisionLayerName() + { + AZStd::string layerName; + if (auto heightfield = GetHeightfieldShape()) + { + Physics::CollisionRequestBus::BroadcastResult( + layerName, &Physics::CollisionRequests::GetCollisionLayerName, heightfield->GetCollisionLayer()); + } + return layerName; + } + + // CollisionFilteringRequestBus + void HeightfieldColliderComponent::SetCollisionGroup(const AZStd::string& groupName, AZ::Crc32 colliderTag) + { + if (auto heightfield = GetHeightfieldShape()) + { + if (Physics::Utils::FilterTag(heightfield->GetTag(), colliderTag)) + { + bool success = false; + AzPhysics::CollisionGroup group; + Physics::CollisionRequestBus::BroadcastResult( + success, &Physics::CollisionRequests::TryGetCollisionGroupByName, groupName, group); + if (success) + { + heightfield->SetCollisionGroup(group); + } + } + } + } + + // CollisionFilteringRequestBus + AZStd::string HeightfieldColliderComponent::GetCollisionGroupName() + { + AZStd::string groupName; + if (auto heightfield = GetHeightfieldShape()) + { + Physics::CollisionRequestBus::BroadcastResult( + groupName, &Physics::CollisionRequests::GetCollisionGroupName, heightfield->GetCollisionGroup()); + } + + return groupName; + } + + // CollisionFilteringRequestBus + void HeightfieldColliderComponent::ToggleCollisionLayer(const AZStd::string& layerName, AZ::Crc32 colliderTag, bool enabled) + { + if (auto heightfield = GetHeightfieldShape()) + { + if (Physics::Utils::FilterTag(heightfield->GetTag(), colliderTag)) + { + bool success = false; + AzPhysics::CollisionLayer layer; + Physics::CollisionRequestBus::BroadcastResult( + success, &Physics::CollisionRequests::TryGetCollisionLayerByName, layerName, layer); + if (success) + { + auto group = heightfield->GetCollisionGroup(); + group.SetLayer(layer, enabled); + heightfield->SetCollisionGroup(group); + } + } + } + } + +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/HeightfieldColliderComponent.h b/Gems/PhysX/Code/Source/HeightfieldColliderComponent.h new file mode 100644 index 0000000000..3213f7a774 --- /dev/null +++ b/Gems/PhysX/Code/Source/HeightfieldColliderComponent.h @@ -0,0 +1,104 @@ +/* + * 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 +#include + +namespace AzPhysics +{ + struct SimulatedBody; +} + +namespace PhysX +{ + class StaticRigidBody; + + //! Component that provides a Heightfield Collider and associated Static Rigid Body. + //! The heightfield collider is a bit different from the other shape colliders in that it gets the heightfield data from a + //! HeightfieldProvider, which can control position, rotation, size, and even change its data at runtime. + //! + //! Due to these differences, this component directly implements both the collider and static rigid body services instead of + //! using BaseColliderComponent and StaticRigidBodyComponent. + class HeightfieldColliderComponent + : public AZ::Component + , public ColliderComponentRequestBus::Handler + , public AzPhysics::SimulatedBodyComponentRequestsBus::Handler + , protected PhysX::ColliderShapeRequestBus::Handler + , protected Physics::CollisionFilteringRequestBus::Handler + , protected Physics::HeightfieldProviderNotificationBus::Handler + { + public: + using Configuration = Physics::HeightfieldShapeConfiguration; + AZ_COMPONENT(HeightfieldColliderComponent, "{9A42672C-281A-4CE8-BFDD-EAA1E0FCED76}"); + static void Reflect(AZ::ReflectContext* context); + + HeightfieldColliderComponent() = default; + ~HeightfieldColliderComponent() override; + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + + void Activate() override; + void Deactivate() override; + + void SetShapeConfiguration(const AzPhysics::ShapeColliderPair& shapeConfig); + + protected: + // ColliderComponentRequestBus + AzPhysics::ShapeColliderPairList GetShapeConfigurations() override; + AZStd::vector> GetShapes() override; + + // ColliderShapeRequestBus + AZ::Aabb GetColliderShapeAabb() override; + bool IsTrigger() override + { + // PhysX Heightfields don't support triggers. + return false; + } + + // CollisionFilteringRequestBus + void SetCollisionLayer(const AZStd::string& layerName, AZ::Crc32 filterTag) override; + AZStd::string GetCollisionLayerName() override; + void SetCollisionGroup(const AZStd::string& groupName, AZ::Crc32 filterTag) override; + AZStd::string GetCollisionGroupName() override; + void ToggleCollisionLayer(const AZStd::string& layerName, AZ::Crc32 filterTag, bool enabled) override; + + // SimulatedBodyComponentRequestsBus + void EnablePhysics() override; + void DisablePhysics() override; + bool IsPhysicsEnabled() const override; + AZ::Aabb GetAabb() const override; + AzPhysics::SimulatedBodyHandle GetSimulatedBodyHandle() const override; + AzPhysics::SimulatedBody* GetSimulatedBody() override; + AzPhysics::SceneQueryHit RayCast(const AzPhysics::RayCastRequest& request) override; + + // HeightfieldProviderNotificationBus + void OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) override; + + private: + AZStd::shared_ptr GetHeightfieldShape(); + + void ClearHeightfield(); + void InitHeightfieldShapeConfiguration(); + void InitStaticRigidBody(); + void RefreshHeightfield(); + + AzPhysics::ShapeColliderPair m_shapeConfig; + AzPhysics::SimulatedBodyHandle m_staticRigidBodyHandle = AzPhysics::InvalidSimulatedBodyHandle; + AzPhysics::SceneHandle m_attachedSceneHandle = AzPhysics::InvalidSceneHandle; + }; +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/SystemComponent.cpp b/Gems/PhysX/Code/Source/SystemComponent.cpp index 2f4d082ccb..000d69d982 100644 --- a/Gems/PhysX/Code/Source/SystemComponent.cpp +++ b/Gems/PhysX/Code/Source/SystemComponent.cpp @@ -252,6 +252,22 @@ namespace PhysX return convex; } + physx::PxHeightField* SystemComponent::CreateHeightField(const physx::PxHeightFieldSample* samples, AZ::u32 numRows, AZ::u32 numColumns) + { + physx::PxHeightFieldDesc desc; + desc.format = physx::PxHeightFieldFormat::eS16_TM; + desc.nbColumns = numColumns; + desc.nbRows = numRows; + desc.samples.data = samples; + desc.samples.stride = sizeof(physx::PxHeightFieldSample); + + physx::PxHeightField* heightfield = + m_physXSystem->GetPxCooking()->createHeightField(desc, m_physXSystem->GetPxPhysics()->getPhysicsInsertionCallback()); + AZ_Error("PhysX", heightfield, "Error. Unable to create heightfield"); + + return heightfield; + } + bool SystemComponent::CookConvexMeshToFile(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount) { AZStd::vector physxData; @@ -342,6 +358,14 @@ namespace PhysX return AZStd::make_shared(materialConfiguration); } + void SystemComponent::ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject) + { + if (nativeHeightfieldObject) + { + static_cast(nativeHeightfieldObject)->release(); + } + } + void SystemComponent::ReleaseNativeMeshObject(void* nativeMeshObject) { if (nativeMeshObject) diff --git a/Gems/PhysX/Code/Source/SystemComponent.h b/Gems/PhysX/Code/Source/SystemComponent.h index 3661655bb3..d581af7e67 100644 --- a/Gems/PhysX/Code/Source/SystemComponent.h +++ b/Gems/PhysX/Code/Source/SystemComponent.h @@ -76,6 +76,7 @@ namespace PhysX physx::PxConvexMesh* CreateConvexMesh(const void* vertices, AZ::u32 vertexNum, AZ::u32 vertexStride) override; // should we use AZ::Vector3* or physx::PxVec3 here? physx::PxConvexMesh* CreateConvexMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) override; physx::PxTriangleMesh* CreateTriangleMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) override; + physx::PxHeightField* CreateHeightField(const physx::PxHeightFieldSample* samples, AZ::u32 numRows, AZ::u32 numColumns) override; bool CookConvexMeshToFile(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount) override; @@ -112,6 +113,7 @@ namespace PhysX AZStd::shared_ptr CreateMaterial(const Physics::MaterialConfiguration& materialConfiguration) override; void ReleaseNativeMeshObject(void* nativeMeshObject) override; + void ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject) override; // Assets related data AZStd::vector> m_assetHandlers; diff --git a/Gems/PhysX/Code/Source/Utils.cpp b/Gems/PhysX/Code/Source/Utils.cpp index 8a76d6d986..b4e1e768c3 100644 --- a/Gems/PhysX/Code/Source/Utils.cpp +++ b/Gems/PhysX/Code/Source/Utils.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -25,6 +26,7 @@ #include #include #include +#include #include #include @@ -64,6 +66,136 @@ namespace PhysX } } + void CreatePxGeometryFromHeightfield( + Physics::HeightfieldShapeConfiguration& heightfieldConfig, physx::PxGeometryHolder& pxGeometry) + { + physx::PxHeightField* heightfield = nullptr; + + const AZ::Vector2 gridSpacing = heightfieldConfig.GetGridResolution(); + + const int32_t numCols = heightfieldConfig.GetNumColumns(); + const int32_t numRows = heightfieldConfig.GetNumRows(); + + const float rowScale = gridSpacing.GetX(); + const float colScale = gridSpacing.GetY(); + + const float minHeightBounds = heightfieldConfig.GetMinHeightBounds(); + const float maxHeightBounds = heightfieldConfig.GetMaxHeightBounds(); + const float halfBounds{ (maxHeightBounds - minHeightBounds) / 2.0f }; + + // We're making the assumption right now that the min/max bounds are centered around 0. + // If we ever want to allow off-center bounds, we'll need to fix up the float-to-int16 height math below + // to account for it. + AZ_Assert( + AZ::IsClose(-halfBounds, minHeightBounds) && AZ::IsClose(halfBounds, maxHeightBounds), + "Min/Max height bounds aren't centered around 0, the height conversions below will be incorrect."); + + AZ_Assert( + maxHeightBounds >= minHeightBounds, + "Max height bounds is less than min height bounds, the height conversions below will be incorrect."); + + // To convert our floating-point heights to fixed-point representation inside of an int16, we need a scale factor + // for the conversion. The scale factor is used to map the most important bits of our floating-point height to the + // full 16-bit range. + // Note that the scaleFactor choice here affects overall precision. For each bit that the integer part of our max + // height uses, that's one less bit for the fractional part. + const float scaleFactor = (maxHeightBounds <= minHeightBounds) ? 1.0f : AZStd::numeric_limits::max() / halfBounds; + const float heightScale{ 1.0f / scaleFactor }; + + const uint8_t physxMaximumMaterialIndex = 0x7f; + + // Delete the cached heightfield object if it is there, and create a new one and save in the shape configuration + heightfieldConfig.SetCachedNativeHeightfield(nullptr); + + const AZStd::vector& samples = heightfieldConfig.GetSamples(); + AZ_Assert(samples.size() == numRows * numCols, "GetHeightsAndMaterials returned wrong sized heightfield"); + + if (!samples.empty()) + { + AZStd::vector physxSamples(samples.size()); + + for (int32_t row = 0; row < numRows; row++) + { + const bool lastRowIndex = (row == (numRows - 1)); + + for (int32_t col = 0; col < numCols; col++) + { + const bool lastColumnIndex = (col == (numCols - 1)); + + auto GetIndex = [numCols](int32_t row, int32_t col) + { + return (row * numCols) + col; + }; + + const int32_t sampleIndex = GetIndex(row, col); + + const Physics::HeightMaterialPoint& currentSample = samples[sampleIndex]; + physx::PxHeightFieldSample& currentPhysxSample = physxSamples[sampleIndex]; + AZ_Assert(currentSample.m_materialIndex < physxMaximumMaterialIndex, "MaterialIndex must be less than 128"); + currentPhysxSample.height = azlossy_cast( + AZ::GetClamp(currentSample.m_height, minHeightBounds, maxHeightBounds) * scaleFactor); + if (lastRowIndex || lastColumnIndex) + { + // In PhysX, the material indices refer to the quad down and to the right of the sample. + // If we're in the last row or last column, there aren't any quads down or to the right, + // so just clear these out. + currentPhysxSample.materialIndex0 = 0; + currentPhysxSample.materialIndex1 = 0; + } + else + { + // Our source data is providing one material index per vertex, but PhysX wants one material index + // per triangle. The heuristic that we'll go with for selecting the material index is to choose + // the material for the vertex that's not on the diagonal of each triangle. + // Ex: A *---* B + // | / | For this, we'll use A for index0 and D for index1. + // C *---* D + // + // Ex: A *---* B + // | \ | For this, we'll use C for index0 and B for index1. + // C *---* D + // + // This is a pretty arbitrary choice, so the heuristic might need to be revisited over time if this + // causes incorrect or unpredictable physics material mappings. + + switch (currentSample.m_quadMeshType) + { + case Physics::QuadMeshType::SubdivideUpperLeftToBottomRight: + currentPhysxSample.materialIndex0 = samples[GetIndex(row + 1, col)].m_materialIndex; + currentPhysxSample.materialIndex1 = samples[GetIndex(row, col + 1)].m_materialIndex; + // Set the tesselation flag to say that we need to go from UL to BR + currentPhysxSample.materialIndex0.setBit(); + break; + case Physics::QuadMeshType::SubdivideBottomLeftToUpperRight: + currentPhysxSample.materialIndex0 = currentSample.m_materialIndex; + currentPhysxSample.materialIndex1 = samples[GetIndex(row + 1, col + 1)].m_materialIndex; + break; + case Physics::QuadMeshType::Hole: + currentPhysxSample.materialIndex0 = physx::PxHeightFieldMaterial::eHOLE; + currentPhysxSample.materialIndex1 = physx::PxHeightFieldMaterial::eHOLE; + break; + default: + AZ_Warning("PhysX Heightfield", false, "Unhandled case in CreatePxGeometryFromConfig"); + currentPhysxSample.materialIndex0 = 0; + currentPhysxSample.materialIndex1 = 0; + break; + } + } + } + } + + SystemRequestsBus::BroadcastResult(heightfield, &SystemRequests::CreateHeightField, physxSamples.data(), numRows, numCols); + } + if (heightfield) + { + heightfieldConfig.SetCachedNativeHeightfield(heightfield); + + physx::PxHeightFieldGeometry hfGeom(heightfield, physx::PxMeshGeometryFlags(), heightScale, rowScale, colScale); + + pxGeometry.storeAny(hfGeom); + } + } + bool CreatePxGeometryFromConfig(const Physics::ShapeConfiguration& shapeConfiguration, physx::PxGeometryHolder& pxGeometry) { if (!shapeConfiguration.m_scale.IsGreaterThan(AZ::Vector3::CreateZero())) @@ -132,9 +264,14 @@ namespace PhysX } case Physics::ShapeType::CookedMesh: { - const Physics::CookedMeshShapeConfiguration& cookedMeshShapeConfig = + const Physics::CookedMeshShapeConfiguration& constCookedMeshShapeConfig = static_cast(shapeConfiguration); + // We are deliberately removing the const off of the ShapeConfiguration here because we're going to change the cached + // native mesh pointer that gets stored in the configuration. + Physics::CookedMeshShapeConfiguration& cookedMeshShapeConfig = + const_cast(constCookedMeshShapeConfig); + physx::PxBase* nativeMeshObject = nullptr; // Use the cached mesh object if it is there, otherwise create one and save in the shape configuration @@ -170,6 +307,19 @@ namespace PhysX "Please iterate over m_colliderShapes in the asset and call this function for each of them."); return false; } + case Physics::ShapeType::Heightfield: + { + const Physics::HeightfieldShapeConfiguration& constHeightfieldConfig = + static_cast(shapeConfiguration); + + // We are deliberately removing the const off of the ShapeConfiguration here because we're going to change the cached + // native heightfield pointer that gets stored in the configuration. + Physics::HeightfieldShapeConfiguration& heightfieldConfig = + const_cast(constHeightfieldConfig); + + CreatePxGeometryFromHeightfield(heightfieldConfig, pxGeometry); + break; + } default: AZ_Warning("PhysX Rigid Body", false, "Shape not supported in PhysX. Shape Type: %d", shapeType); return false; @@ -219,6 +369,26 @@ namespace PhysX physx::PxQuat pxQuat(AZ::Constants::HalfPi, physx::PxVec3(0.0f, 1.0f, 0.0f)); shape->setLocalPose(physx::PxTransform(pxQuat)); } + else if (pxGeomHolder.getType() == physx::PxGeometryType::eHEIGHTFIELD) + { + const Physics::HeightfieldShapeConfiguration& heightfieldConfig = + static_cast(shapeConfiguration); + + // PhysX heightfields have the origin at the corner, not the center, so add an offset to the passed-in transform + // to account for this difference. + const AZ::Vector2 gridSpacing = heightfieldConfig.GetGridResolution(); + AZ::Vector3 offset( + -(gridSpacing.GetX() * heightfieldConfig.GetNumColumns() / 2.0f), + -(gridSpacing.GetY() * heightfieldConfig.GetNumRows() / 2.0f), + 0.0f); + + // PhysX heightfields are always defined to have the height in the Y direction, not the Z direction, so we need + // to provide additional rotations to make it Z-up. + physx::PxQuat pxQuat = PxMathConvert( + AZ::Quaternion::CreateFromEulerAnglesRadians(AZ::Vector3(AZ::Constants::HalfPi, AZ::Constants::HalfPi, 0.0f))); + physx::PxTransform pxHeightfieldTransform = physx::PxTransform(PxMathConvert(offset), pxQuat); + shape->setLocalPose(pxHeightfieldTransform); + } // Handle a possible misconfiguration when a shape is set to be both simulated & trigger. This is illegal in PhysX. shape->setFlag(physx::PxShapeFlag::eSIMULATION_SHAPE, colliderConfiguration.m_isSimulated && !colliderConfiguration.m_isTrigger); @@ -1357,6 +1527,41 @@ namespace PhysX return entityWorldTransformWithoutScale * jointLocalTransformWithoutScale; } + + Physics::HeightfieldShapeConfiguration CreateHeightfieldShapeConfiguration(AZ::EntityId entityId) + { + Physics::HeightfieldShapeConfiguration configuration; + + AZ::Vector2 gridSpacing(1.0f); + Physics::HeightfieldProviderRequestsBus::EventResult( + gridSpacing, entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSpacing); + + configuration.SetGridResolution(gridSpacing); + + int32_t numRows = 0; + int32_t numColumns = 0; + Physics::HeightfieldProviderRequestsBus::Event( + entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, numColumns, numRows); + + configuration.SetNumRows(numRows); + configuration.SetNumColumns(numColumns); + + float minHeightBounds = 0.0f; + float maxHeightBounds = 0.0f; + Physics::HeightfieldProviderRequestsBus::Event( + entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldHeightBounds, minHeightBounds, maxHeightBounds); + + configuration.SetMinHeightBounds(minHeightBounds); + configuration.SetMaxHeightBounds(maxHeightBounds); + + AZStd::vector samples; + Physics::HeightfieldProviderRequestsBus::EventResult( + samples, entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials); + + configuration.SetSamples(samples); + + return configuration; + } } // namespace Utils namespace ReflectionUtils diff --git a/Gems/PhysX/Code/Source/Utils.h b/Gems/PhysX/Code/Source/Utils.h index ed63605bc8..525db03315 100644 --- a/Gems/PhysX/Code/Source/Utils.h +++ b/Gems/PhysX/Code/Source/Utils.h @@ -188,6 +188,8 @@ namespace PhysX //! Returns defaultValue if the input is infinite or NaN, otherwise returns the input unchanged. const AZ::Vector3& Sanitize(const AZ::Vector3& input, const AZ::Vector3& defaultValue = AZ::Vector3::CreateZero()); + Physics::HeightfieldShapeConfiguration CreateHeightfieldShapeConfiguration(AZ::EntityId entityId); + namespace Geometry { using PointList = AZStd::vector; diff --git a/Gems/PhysX/Code/Tests/Benchmarks/PhysXBenchmarksCommon.cpp b/Gems/PhysX/Code/Tests/Benchmarks/PhysXBenchmarksCommon.cpp index 70b5aefe14..49dc249d25 100644 --- a/Gems/PhysX/Code/Tests/Benchmarks/PhysXBenchmarksCommon.cpp +++ b/Gems/PhysX/Code/Tests/Benchmarks/PhysXBenchmarksCommon.cpp @@ -61,7 +61,6 @@ namespace PhysX::Benchmarks m_defaultScene = physicsSystem->GetScene(m_testSceneHandle); } - m_dummyTerrainComponentDescriptor = DummyTestTerrainComponent::CreateDescriptor(); Physics::DefaultWorldBus::Handler::BusConnect(); } @@ -80,9 +79,6 @@ namespace PhysX::Benchmarks m_testSceneHandle = AzPhysics::InvalidSceneHandle; TestUtils::ResetPhysXSystem(); - - m_dummyTerrainComponentDescriptor->ReleaseDescriptor(); - m_dummyTerrainComponentDescriptor = nullptr; } AzPhysics::SceneHandle PhysXBaseBenchmarkFixture::CreateDefaultTestScene() diff --git a/Gems/PhysX/Code/Tests/Benchmarks/PhysXBenchmarksCommon.h b/Gems/PhysX/Code/Tests/Benchmarks/PhysXBenchmarksCommon.h index b32298d484..bde4995e27 100644 --- a/Gems/PhysX/Code/Tests/Benchmarks/PhysXBenchmarksCommon.h +++ b/Gems/PhysX/Code/Tests/Benchmarks/PhysXBenchmarksCommon.h @@ -61,7 +61,6 @@ namespace PhysX::Benchmarks AzPhysics::Scene* m_defaultScene = nullptr; AzPhysics::SceneHandle m_testSceneHandle = AzPhysics::InvalidSceneHandle; - AZ::ComponentDescriptor* m_dummyTerrainComponentDescriptor = nullptr; }; } // namespace PhysX::Benchmarks #endif //HAVE_BENCHMARK diff --git a/Gems/PhysX/Code/Tests/EditorTestUtilities.cpp b/Gems/PhysX/Code/Tests/EditorTestUtilities.cpp index 63a689894c..fa794c58a5 100644 --- a/Gems/PhysX/Code/Tests/EditorTestUtilities.cpp +++ b/Gems/PhysX/Code/Tests/EditorTestUtilities.cpp @@ -52,14 +52,10 @@ namespace PhysXEditorTests m_defaultScene = physicsSystem->GetScene(m_defaultSceneHandle); } Physics::DefaultWorldBus::Handler::BusConnect(); - m_dummyTerrainComponentDescriptor = PhysX::DummyTestTerrainComponent::CreateDescriptor(); } void PhysXEditorFixture::TearDown() { - m_dummyTerrainComponentDescriptor->ReleaseDescriptor(); - m_dummyTerrainComponentDescriptor = nullptr; - Physics::DefaultWorldBus::Handler::BusDisconnect(); // prevents warnings from the undo cache on subsequent tests diff --git a/Gems/PhysX/Code/Tests/EditorTestUtilities.h b/Gems/PhysX/Code/Tests/EditorTestUtilities.h index b05a7561fe..9dfe40f366 100644 --- a/Gems/PhysX/Code/Tests/EditorTestUtilities.h +++ b/Gems/PhysX/Code/Tests/EditorTestUtilities.h @@ -51,7 +51,6 @@ namespace PhysXEditorTests // DefaultWorldBus AzPhysics::SceneHandle GetDefaultSceneHandle() const override; - AZ::ComponentDescriptor* m_dummyTerrainComponentDescriptor = nullptr; AzPhysics::SceneHandle m_defaultSceneHandle = AzPhysics::InvalidSceneHandle; AzPhysics::Scene* m_defaultScene = nullptr; diff --git a/Gems/PhysX/Code/Tests/PhysXTestFixtures.cpp b/Gems/PhysX/Code/Tests/PhysXTestFixtures.cpp index b3f233d388..c5fb2d5092 100644 --- a/Gems/PhysX/Code/Tests/PhysXTestFixtures.cpp +++ b/Gems/PhysX/Code/Tests/PhysXTestFixtures.cpp @@ -41,16 +41,12 @@ namespace PhysX } Physics::DefaultWorldBus::Handler::BusConnect(); - - m_dummyTerrainComponentDescriptor = DummyTestTerrainComponent::CreateDescriptor(); } void PhysXDefaultWorldTest::TearDown() { Physics::DefaultWorldBus::Handler::BusDisconnect(); m_defaultScene = nullptr; - m_dummyTerrainComponentDescriptor->ReleaseDescriptor(); - m_dummyTerrainComponentDescriptor = nullptr; //Clean up the Test scene if (auto* physicsSystem = AZ::Interface::Get()) diff --git a/Gems/PhysX/Code/Tests/PhysXTestFixtures.h b/Gems/PhysX/Code/Tests/PhysXTestFixtures.h index 9b00f7d892..6a8325a66f 100644 --- a/Gems/PhysX/Code/Tests/PhysXTestFixtures.h +++ b/Gems/PhysX/Code/Tests/PhysXTestFixtures.h @@ -33,7 +33,6 @@ namespace PhysX // DefaultWorldBus AzPhysics::SceneHandle GetDefaultSceneHandle() const override; - AZ::ComponentDescriptor* m_dummyTerrainComponentDescriptor = nullptr; AzPhysics::Scene* m_defaultScene = nullptr; AzPhysics::SceneHandle m_testSceneHandle = AzPhysics::InvalidSceneHandle; }; diff --git a/Gems/PhysX/Code/Tests/PhysXTestUtil.h b/Gems/PhysX/Code/Tests/PhysXTestUtil.h index 1c86768de7..d821fb146b 100644 --- a/Gems/PhysX/Code/Tests/PhysXTestUtil.h +++ b/Gems/PhysX/Code/Tests/PhysXTestUtil.h @@ -63,68 +63,4 @@ namespace PhysX AzPhysics::SimulatedBodyEvents::OnTriggerExit::Handler m_onTriggerExitHandler; }; - - //! Dummy component emulating presence of terrain by connecting to TerrainDataRequestBus - //! PhysX Terrain Component skips activation if there's no terrain present, - //! so in order to test it we also add the DummyTestTerrainComponent. - class DummyTestTerrainComponent - : public AZ::Component - , private AzFramework::Terrain::TerrainDataRequestBus::Handler - { - public: - AZ_COMPONENT(DummyTestTerrainComponent, "{EE4ECA23-9C27-4D5D-9C6F-271A19C0333E}"); - static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) - { - provided.push_back(AZ_CRC_CE("TerrainService")); - } - - private: - //////////////////////////////////////////////////////////////////////// - // AZ::Component interface implementation - void Activate() override - { - AzFramework::Terrain::TerrainDataRequestBus::Handler::BusConnect(); - } - void Deactivate() override - { - AzFramework::Terrain::TerrainDataRequestBus::Handler::BusDisconnect(); - } - //////////////////////////////////////////////////////////////////////// - - //////////////////////////////////////////////////////////////////////// - // TerrainDataRequestBus interface dummy implementation - AZ::Vector2 GetTerrainHeightQueryResolution() const override - { - return {}; - } - void SetTerrainHeightQueryResolution([[maybe_unused]] AZ::Vector2 queryResolution) override - { - } - - AZ::Aabb GetTerrainAabb() const override - { - return {}; - } - void SetTerrainAabb([[maybe_unused]] const AZ::Aabb& worldBounds) override - { - } - - float GetHeight(AZ::Vector3, Sampler, bool*) const override - { - return {}; - } - float GetHeightFromFloats(float, float, Sampler, bool*) const override { return {}; } - AzFramework::SurfaceData::SurfaceTagWeight GetMaxSurfaceWeight(AZ::Vector3, Sampler, bool*) const override { return {}; } - AzFramework::SurfaceData::SurfaceTagWeight GetMaxSurfaceWeightFromVector2(const AZ::Vector2&, Sampler, bool*) const override { return {}; } - AzFramework::SurfaceData::SurfaceTagWeight GetMaxSurfaceWeightFromFloats(float, float, Sampler, bool*) const override { return {}; } - void GetSurfaceWeights(const AZ::Vector3&, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&, Sampler, bool*) const override {} - void GetSurfaceWeightsFromVector2(const AZ::Vector2&, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&, Sampler, bool*) const override{}; - void GetSurfaceWeightsFromFloats(float, float, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&, Sampler, bool*) const override {} - const char* GetMaxSurfaceName(AZ::Vector3, Sampler, bool*) const override { return {}; } - bool GetIsHoleFromFloats(float, float, Sampler) const override { return {}; } - AZ::Vector3 GetNormal(AZ::Vector3, Sampler, bool*) const override { return {}; } - AZ::Vector3 GetNormalFromFloats(float, float, Sampler, bool*) const override { return {}; } - //////////////////////////////////////////////////////////////////////// - }; - } // namespace PhysX diff --git a/Gems/PhysX/Code/physx_editor_files.cmake b/Gems/PhysX/Code/physx_editor_files.cmake index 5c0c038976..1b22c0ce99 100644 --- a/Gems/PhysX/Code/physx_editor_files.cmake +++ b/Gems/PhysX/Code/physx_editor_files.cmake @@ -27,6 +27,8 @@ set(FILES Source/EditorFixedJointComponent.h Source/EditorHingeJointComponent.cpp Source/EditorHingeJointComponent.h + Source/EditorHeightfieldColliderComponent.cpp + Source/EditorHeightfieldColliderComponent.h Source/EditorJointComponent.cpp Source/EditorJointComponent.h Source/Pipeline/MeshExporter.cpp diff --git a/Gems/PhysX/Code/physx_files.cmake b/Gems/PhysX/Code/physx_files.cmake index 654f1d9767..efefaf3105 100644 --- a/Gems/PhysX/Code/physx_files.cmake +++ b/Gems/PhysX/Code/physx_files.cmake @@ -35,6 +35,8 @@ set(FILES Source/MeshColliderComponent.h Source/BoxColliderComponent.h Source/BoxColliderComponent.cpp + Source/HeightfieldColliderComponent.h + Source/HeightfieldColliderComponent.cpp Source/SphereColliderComponent.h Source/SphereColliderComponent.cpp Source/CapsuleColliderComponent.h diff --git a/Gems/Profiler/Code/Source/ProfilerImGuiModule.cpp b/Gems/Profiler/Code/Source/ProfilerImGuiModule.cpp index 38cfee2c26..f7a3d4d69d 100644 --- a/Gems/Profiler/Code/Source/ProfilerImGuiModule.cpp +++ b/Gems/Profiler/Code/Source/ProfilerImGuiModule.cpp @@ -46,4 +46,4 @@ namespace Profiler }; }// namespace Profiler -AZ_DECLARE_MODULE_CLASS(Gem_Profiler, Profiler::ProfilerImGuiModule) +AZ_DECLARE_MODULE_CLASS(Gem_ProfilerImGui, Profiler::ProfilerImGuiModule) diff --git a/Gems/QtForPython/Editor/Scripts/az_qt_helpers.py b/Gems/QtForPython/Editor/Scripts/az_qt_helpers.py index 6b9fbc8f08..758cc539d7 100755 --- a/Gems/QtForPython/Editor/Scripts/az_qt_helpers.py +++ b/Gems/QtForPython/Editor/Scripts/az_qt_helpers.py @@ -18,6 +18,14 @@ from shiboken2 import wrapInstance, getCppPointer view_pane_handlers = {} registration_handlers = {} +# Helper method for retrieving the Editor QMainWindow instance +def get_editor_main_window(): + params = azlmbr.qt.QtForPythonRequestBus(azlmbr.bus.Broadcast, "GetQtBootstrapParameters") + editor_id = QtWidgets.QWidget.find(params.mainWindowId) + editor_main_window = wrapInstance(int(getCppPointer(editor_id)[0]), QtWidgets.QMainWindow) + + return editor_main_window + # Helper method for registering a Python widget as a tool/view pane with the Editor def register_view_pane(name, widget_type, options=editor.ViewPaneOptions()): global view_pane_handlers @@ -29,9 +37,7 @@ def register_view_pane(name, widget_type, options=editor.ViewPaneOptions()): # This method will be invoked by the ViewPaneCallbackBus::CreateViewPaneWidget # when our view pane needs to be created def on_create_view_pane_widget(parameters): - params = azlmbr.qt.QtForPythonRequestBus(azlmbr.bus.Broadcast, "GetQtBootstrapParameters") - editor_id = QtWidgets.QWidget.find(params.mainWindowId) - editor_main_window = wrapInstance(int(getCppPointer(editor_id)[0]), QtWidgets.QMainWindow) + editor_main_window = get_editor_main_window() dock_main_window = editor_main_window.findChild(QtWidgets.QMainWindow) # Create the view pane widget parented to the Editor QMainWindow, so it can be found diff --git a/Gems/ScriptCanvas/Code/Editor/Components/EditorGraph.cpp b/Gems/ScriptCanvas/Code/Editor/Components/EditorGraph.cpp index f003618c03..78325e9a90 100644 --- a/Gems/ScriptCanvas/Code/Editor/Components/EditorGraph.cpp +++ b/Gems/ScriptCanvas/Code/Editor/Components/EditorGraph.cpp @@ -19,6 +19,7 @@ AZ_POP_DISABLE_WARNING #include #include #include +#include #include #include #include @@ -3310,7 +3311,7 @@ namespace ScriptCanvasEditor void Graph::OnToastInteraction() { - const GraphCanvas::ToastId* toastId = GraphCanvas::ToastNotificationBus::GetCurrentBusId(); + const AzToolsFramework::ToastId* toastId = AzToolsFramework::ToastNotificationBus::GetCurrentBusId(); if (toastId) { @@ -3335,7 +3336,7 @@ namespace ScriptCanvasEditor void Graph::OnToastDismissed() { - const GraphCanvas::ToastId* toastId = GraphCanvas::ToastNotificationBus::GetCurrentBusId(); + const AzToolsFramework::ToastId* toastId = AzToolsFramework::ToastNotificationBus::GetCurrentBusId(); if (toastId) { @@ -3358,25 +3359,21 @@ namespace ScriptCanvasEditor void Graph::ReportError(const ScriptCanvas::Node& node, const AZStd::string& errorSource, const AZStd::string& errorMessage) { - GraphCanvas::ToastConfiguration toastConfiguration(GraphCanvas::ToastType::Error, errorSource, errorMessage); - - toastConfiguration.SetCloseOnClick(true); - toastConfiguration.SetDuration(AZStd::chrono::milliseconds(5000)); + AzQtComponents::ToastConfiguration toastConfiguration(AzQtComponents::ToastType::Error, errorSource.c_str(), errorMessage.c_str()); GraphCanvas::ViewId viewId; GraphCanvas::SceneRequestBus::EventResult(viewId, GetGraphCanvasGraphId(), &GraphCanvas::SceneRequests::GetViewId); - GraphCanvas::ToastId toastId; - + AzToolsFramework::ToastId toastId; GraphCanvas::ViewRequestBus::EventResult(toastId, viewId, &GraphCanvas::ViewRequests::ShowToastNotification, toastConfiguration); - GraphCanvas::ToastNotificationBus::MultiHandler::BusConnect(toastId); + AzToolsFramework::ToastNotificationBus::MultiHandler::BusConnect(toastId); m_toastNodeIds[toastId] = node.GetEntityId(); } - void Graph::UnregisterToast(const GraphCanvas::ToastId& toastId) + void Graph::UnregisterToast(const AzToolsFramework::ToastId& toastId) { - GraphCanvas::ToastNotificationBus::MultiHandler::BusDisconnect(toastId); + AzToolsFramework::ToastNotificationBus::MultiHandler::BusDisconnect(toastId); m_toastNodeIds.erase(toastId); } @@ -3408,10 +3405,7 @@ namespace ScriptCanvasEditor m_updateStrings.clear(); - GraphCanvas::ToastConfiguration toastConfiguration(GraphCanvas::ToastType::Information, "Nodes Updates", displayString); - toastConfiguration.SetCloseOnClick(true); - toastConfiguration.SetDuration(AZStd::chrono::milliseconds(5000)); - + AzQtComponents::ToastConfiguration toastConfiguration(AzQtComponents::ToastType::Information, "Nodes Updates", displayString.c_str()); GraphCanvas::ViewRequestBus::Event(viewId, &GraphCanvas::ViewRequests::ShowToastNotification, toastConfiguration); } } diff --git a/Gems/ScriptCanvas/Code/Editor/Include/ScriptCanvas/Components/EditorGraph.h b/Gems/ScriptCanvas/Code/Editor/Include/ScriptCanvas/Components/EditorGraph.h index 6f73e84493..9913fa838b 100644 --- a/Gems/ScriptCanvas/Code/Editor/Include/ScriptCanvas/Components/EditorGraph.h +++ b/Gems/ScriptCanvas/Code/Editor/Include/ScriptCanvas/Components/EditorGraph.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -27,7 +28,6 @@ #include #include #include -#include #include #include @@ -52,7 +52,7 @@ namespace ScriptCanvasEditor , private GraphCanvas::GraphModelRequestBus::Handler , private GraphCanvas::SceneNotificationBus::Handler , private GraphItemCommandNotificationBus::Handler - , private GraphCanvas::ToastNotificationBus::MultiHandler + , private AzToolsFramework::ToastNotificationBus::MultiHandler , private GeneralEditorNotificationBus::Handler , private AZ::SystemTickBus::Handler { @@ -327,7 +327,7 @@ namespace ScriptCanvasEditor protected: void PostRestore(const UndoData& restoredData) override; - void UnregisterToast(const GraphCanvas::ToastId& toastId); + void UnregisterToast(const AzToolsFramework::ToastId& toastId); Graph(const Graph&) = delete; @@ -350,7 +350,7 @@ namespace ScriptCanvasEditor void HandleQueuedUpdates(); bool IsNodeVersionConverting(const AZ::EntityId& graphCanvasNodeId) const; - AZStd::unordered_map< GraphCanvas::ToastId, AZ::EntityId > m_toastNodeIds; + AZStd::unordered_map< AzToolsFramework::ToastId, AZ::EntityId > m_toastNodeIds; // Function Definition Node Extension void HandleFunctionDefinitionExtension(ScriptCanvas::Node* node, GraphCanvas::SlotId graphCanvasSlotId, const GraphCanvas::NodeId& nodeId); diff --git a/Gems/ScriptCanvas/Code/Editor/View/Widgets/ValidationPanel/GraphValidationDockWidget.cpp b/Gems/ScriptCanvas/Code/Editor/View/Widgets/ValidationPanel/GraphValidationDockWidget.cpp index ded0130001..33de6c0e2c 100644 --- a/Gems/ScriptCanvas/Code/Editor/View/Widgets/ValidationPanel/GraphValidationDockWidget.cpp +++ b/Gems/ScriptCanvas/Code/Editor/View/Widgets/ValidationPanel/GraphValidationDockWidget.cpp @@ -8,6 +8,7 @@ #include +#include #include #include #include @@ -1434,31 +1435,28 @@ namespace ScriptCanvasEditor GraphCanvas::ViewId viewId; GraphCanvas::SceneRequestBus::EventResult(viewId, m_graphCanvasId, &GraphCanvas::SceneRequests::GetViewId); - GraphCanvas::ToastType toastType; + AzQtComponents::ToastType toastType; AZStd::string titleLabel = "Validation Issue"; AZStd::string description = ""; if (m_model->GetValidationResults().HasErrors()) { - toastType = GraphCanvas::ToastType::Error; + toastType = AzQtComponents::ToastType::Error; description = AZStd::string::format("%i validation error(s) were found.", m_model->GetValidationResults().ErrorCount()); } else { - toastType = GraphCanvas::ToastType::Warning; + toastType = AzQtComponents::ToastType::Warning; description = AZStd::string::format("%i validation warning(s) were found.", m_model->GetValidationResults().WarningCount()); } - GraphCanvas::ToastConfiguration toastConfiguration(toastType, titleLabel, description); + AzQtComponents::ToastConfiguration toastConfiguration(toastType, titleLabel.c_str(), description.c_str()); - toastConfiguration.SetCloseOnClick(true); - toastConfiguration.SetDuration(AZStd::chrono::milliseconds(5000)); - - GraphCanvas::ToastId validationToastId; + AzToolsFramework::ToastId validationToastId; GraphCanvas::ViewRequestBus::EventResult(validationToastId, viewId, &GraphCanvas::ViewRequests::ShowToastNotification, toastConfiguration); - GraphCanvas::ToastNotificationBus::MultiHandler::BusConnect(validationToastId); + AzToolsFramework::ToastNotificationBus::MultiHandler::BusConnect(validationToastId); } @@ -1469,11 +1467,11 @@ namespace ScriptCanvasEditor void ValidationData::OnToastDismissed() { - const GraphCanvas::ToastId* toastId = GraphCanvas::ToastNotificationBus::GetCurrentBusId(); + const AzToolsFramework::ToastId* toastId = AzToolsFramework::ToastNotificationBus::GetCurrentBusId(); if (toastId) { - GraphCanvas::ToastNotificationBus::MultiHandler::BusDisconnect((*toastId)); + AzToolsFramework::ToastNotificationBus::MultiHandler::BusDisconnect((*toastId)); } } diff --git a/Gems/ScriptCanvas/Code/Editor/View/Widgets/ValidationPanel/GraphValidationDockWidget.h b/Gems/ScriptCanvas/Code/Editor/View/Widgets/ValidationPanel/GraphValidationDockWidget.h index 107cfe7c10..66eecadaf8 100644 --- a/Gems/ScriptCanvas/Code/Editor/View/Widgets/ValidationPanel/GraphValidationDockWidget.h +++ b/Gems/ScriptCanvas/Code/Editor/View/Widgets/ValidationPanel/GraphValidationDockWidget.h @@ -15,10 +15,10 @@ #include #include +#include #include #include -#include #include #include @@ -183,7 +183,7 @@ namespace ScriptCanvasEditor //! Owns the model for each currently opened graph class ValidationData : private ScriptCanvas::StatusRequestBus::Handler - , private GraphCanvas::ToastNotificationBus::MultiHandler + , private AzToolsFramework::ToastNotificationBus::MultiHandler { public: AZ_CLASS_ALLOCATOR(ValidationData, AZ::SystemAllocator, 0); @@ -227,7 +227,7 @@ namespace ScriptCanvasEditor : public AzQtComponents::StyledDockWidget , public GraphCanvas::AssetEditorNotificationBus::Handler , public GraphCanvas::SceneNotificationBus::Handler - , public GraphCanvas::ToastNotificationBus::Handler + , public AzToolsFramework::ToastNotificationBus::Handler { Q_OBJECT public: diff --git a/Gems/ScriptCanvas/Code/Editor/View/Windows/MainWindow.h b/Gems/ScriptCanvas/Code/Editor/View/Windows/MainWindow.h index 2c84412e28..7672f0a199 100644 --- a/Gems/ScriptCanvas/Code/Editor/View/Windows/MainWindow.h +++ b/Gems/ScriptCanvas/Code/Editor/View/Windows/MainWindow.h @@ -27,6 +27,7 @@ #include #include +#include #include #include @@ -124,7 +125,7 @@ namespace ScriptCanvasEditor public: OnSaveToast(AZStd::string_view tabName, AZ::EntityId graphCanvasGraphId, bool saveSuccessful) { - GraphCanvas::ToastType toastType = GraphCanvas::ToastType::Information; + AzQtComponents::ToastType toastType = AzQtComponents::ToastType::Information; AZStd::string titleLabel = "Notification"; AZStd::string description; @@ -136,15 +137,12 @@ namespace ScriptCanvasEditor else { description = AZStd::string::format("Failed to save %s", tabName.data()); - toastType = GraphCanvas::ToastType::Error; + toastType = AzQtComponents::ToastType::Error; } - GraphCanvas::ToastConfiguration toastConfiguration(toastType, titleLabel, description); + AzQtComponents::ToastConfiguration toastConfiguration(toastType, titleLabel.c_str(), description.c_str()); - toastConfiguration.SetCloseOnClick(true); - toastConfiguration.SetDuration(AZStd::chrono::milliseconds(5000)); - - GraphCanvas::ToastId validationToastId; + AzToolsFramework::ToastId validationToastId; GraphCanvas::ViewId viewId; GraphCanvas::SceneRequestBus::EventResult(viewId, graphCanvasGraphId, &GraphCanvas::SceneRequests::GetViewId); diff --git a/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype b/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype index ec04412fe6..01b862c4f8 100644 --- a/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype +++ b/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype @@ -1,7 +1,7 @@ { "description": "A material for rendering terrain with a physically-based rendering (PBR) material shading model.", + "version": 1, "propertyLayout": { - "version": 1, "groups": [ { "id": "settings", diff --git a/Gems/Terrain/Assets/Materials/Terrain/TerrainMacroMaterial.materialtype b/Gems/Terrain/Assets/Materials/Terrain/TerrainMacroMaterial.materialtype index 3cdab8da10..17769ffb92 100644 --- a/Gems/Terrain/Assets/Materials/Terrain/TerrainMacroMaterial.materialtype +++ b/Gems/Terrain/Assets/Materials/Terrain/TerrainMacroMaterial.materialtype @@ -1,7 +1,7 @@ { "description": "A material for providing terrain with low-fidelity color and normals. This material will get blended with surface detail materials.", + "version": 1, "propertyLayout": { - "version": 1, "groups": [ { "name": "baseColor", diff --git a/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl b/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl index 91e367500b..750cd2fb29 100644 --- a/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl +++ b/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl @@ -59,6 +59,7 @@ VSOutput TerrainPBR_MainPassVS(VertexInput IN) DirectionalLightShadow::GetShadowCoords( shadowIndex, worldPosition, + OUT.m_normal, OUT.m_shadowCoords); } diff --git a/Gems/Terrain/Assets/Shaders/Terrain/Terrain_Shadowmap.shader b/Gems/Terrain/Assets/Shaders/Terrain/Terrain_Shadowmap.shader index 290c83bddf..8f163d25cd 100644 --- a/Gems/Terrain/Assets/Shaders/Terrain/Terrain_Shadowmap.shader +++ b/Gems/Terrain/Assets/Shaders/Terrain/Terrain_Shadowmap.shader @@ -1,5 +1,5 @@ { - "Source" : "Terrain_DepthPass", + "Source" : "Terrain_DepthPass.azsl", "DepthStencilState" : { "Depth" : { "Enable" : true, "CompareFunc" : "LessEqual" } diff --git a/Gems/Terrain/Code/CMakeLists.txt b/Gems/Terrain/Code/CMakeLists.txt index 442093a885..4b2f32e172 100644 --- a/Gems/Terrain/Code/CMakeLists.txt +++ b/Gems/Terrain/Code/CMakeLists.txt @@ -111,6 +111,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) AZ::AzTest AZ::AzFramework Gem::LmbrCentral.Mocks + Gem::GradientSignal.Mocks Gem::Terrain.Mocks Gem::Terrain.Static ) diff --git a/Gems/Terrain/Code/Include/Terrain/Ebuses/TerrainAreaSurfaceRequestBus.h b/Gems/Terrain/Code/Include/Terrain/Ebuses/TerrainAreaSurfaceRequestBus.h index 4c04cf1e42..ed352b55df 100644 --- a/Gems/Terrain/Code/Include/Terrain/Ebuses/TerrainAreaSurfaceRequestBus.h +++ b/Gems/Terrain/Code/Include/Terrain/Ebuses/TerrainAreaSurfaceRequestBus.h @@ -27,8 +27,8 @@ namespace Terrain virtual ~TerrainAreaSurfaceRequests() = default; - //! Get the surfaces and weights from a gradient at a given position sorted into descending order of weight. - virtual void GetSurfaceWeights(const AZ::Vector3& inPosition, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights) const = 0; + //! Get the surfaces and weights from a gradient at a given position. + virtual void GetSurfaceWeights(const AZ::Vector3& inPosition, AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights) const = 0; }; using TerrainAreaSurfaceRequestBus = AZ::EBus; diff --git a/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h b/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h index ea0413be9a..53d93be8ea 100644 --- a/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h +++ b/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h @@ -10,11 +10,12 @@ #include #include +#include #include +#include namespace UnitTest { - class MockTerrainSystemService : private Terrain::TerrainSystemServiceRequestBus::Handler { public: @@ -69,11 +70,7 @@ namespace UnitTest Terrain::TerrainAreaHeightRequestBus::Handler::BusDisconnect(); } - MOCK_METHOD3(GetHeight, void( - const AZ::Vector3& inPosition, - AZ::Vector3& outPosition, - bool& terrainExists)); - + MOCK_METHOD3(GetHeight, void(const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists)); }; class MockTerrainSpawnerRequests : public Terrain::TerrainSpawnerRequestBus::Handler @@ -92,4 +89,46 @@ namespace UnitTest MOCK_METHOD2(GetPriority, void(AZ::u32& outLayer, AZ::u32& outPriority)); MOCK_METHOD0(GetUseGroundPlane, bool()); }; -} + + class MockTerrainDataRequests : public AzFramework::Terrain::TerrainDataRequestBus::Handler + { + public: + MockTerrainDataRequests() + { + AzFramework::Terrain::TerrainDataRequestBus::Handler::BusConnect(); + } + + ~MockTerrainDataRequests() + { + AzFramework::Terrain::TerrainDataRequestBus::Handler::BusDisconnect(); + } + + MOCK_CONST_METHOD0(GetTerrainHeightQueryResolution, AZ::Vector2()); + MOCK_METHOD1(SetTerrainHeightQueryResolution, void(AZ::Vector2)); + MOCK_CONST_METHOD0(GetTerrainAabb, AZ::Aabb()); + MOCK_METHOD1(SetTerrainAabb, void(const AZ::Aabb&)); + MOCK_CONST_METHOD3(GetHeight, float(const AZ::Vector3&, Sampler, bool*)); + MOCK_CONST_METHOD3(GetHeightFromVector2, float(const AZ::Vector2&, Sampler, bool*)); + MOCK_CONST_METHOD4(GetHeightFromFloats, float(float, float, Sampler, bool*)); + MOCK_CONST_METHOD2(GetIsHole, bool(const AZ::Vector3&, Sampler)); + MOCK_CONST_METHOD2(GetIsHoleFromVector2, bool(const AZ::Vector2&, Sampler)); + MOCK_CONST_METHOD3(GetIsHoleFromFloats, bool(float, float, Sampler)); + MOCK_CONST_METHOD3(GetNormal, AZ::Vector3(const AZ::Vector3&, Sampler, bool*)); + MOCK_CONST_METHOD3(GetNormalFromVector2, AZ::Vector3(const AZ::Vector2&, Sampler, bool*)); + MOCK_CONST_METHOD4(GetNormalFromFloats, AZ::Vector3(float, float, Sampler, bool*)); + MOCK_CONST_METHOD3(GetMaxSurfaceWeight, AzFramework::SurfaceData::SurfaceTagWeight(const AZ::Vector3&, Sampler, bool*)); + MOCK_CONST_METHOD3(GetMaxSurfaceWeightFromVector2, AzFramework::SurfaceData::SurfaceTagWeight(const AZ::Vector2&, Sampler, bool*)); + MOCK_CONST_METHOD4(GetMaxSurfaceWeightFromFloats, AzFramework::SurfaceData::SurfaceTagWeight(float, float, Sampler, bool*)); + MOCK_CONST_METHOD4(GetSurfaceWeights, void(const AZ::Vector3&, AzFramework::SurfaceData::SurfaceTagWeightList&, Sampler, bool*)); + MOCK_CONST_METHOD4( + GetSurfaceWeightsFromVector2, void(const AZ::Vector2&, AzFramework::SurfaceData::SurfaceTagWeightList&, Sampler, bool*)); + MOCK_CONST_METHOD5( + GetSurfaceWeightsFromFloats, void(float, float, AzFramework::SurfaceData::SurfaceTagWeightList&, Sampler, bool*)); + MOCK_CONST_METHOD3(GetMaxSurfaceName, const char*(const AZ::Vector3&, Sampler, bool*)); + MOCK_CONST_METHOD4(GetSurfacePoint, void(const AZ::Vector3&, AzFramework::SurfaceData::SurfacePoint&, Sampler, bool*)); + MOCK_CONST_METHOD4( + GetSurfacePointFromVector2, void(const AZ::Vector2&, AzFramework::SurfaceData::SurfacePoint&, Sampler, bool*)); + MOCK_CONST_METHOD5( + GetSurfacePointFromFloats, void(float, float, AzFramework::SurfaceData::SurfacePoint&, Sampler, bool*)); + }; +} // namespace UnitTest diff --git a/Gems/Terrain/Code/Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h b/Gems/Terrain/Code/Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h new file mode 100644 index 0000000000..665bf581bc --- /dev/null +++ b/Gems/Terrain/Code/Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.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 + +#include +#include +#include + +namespace UnitTest +{ + class MockTerrainAreaSurfaceRequestBus : public Terrain::TerrainAreaSurfaceRequestBus::Handler + { + public: + MockTerrainAreaSurfaceRequestBus(AZ::EntityId entityId) + { + Terrain::TerrainAreaSurfaceRequestBus::Handler::BusConnect(entityId); + } + + ~MockTerrainAreaSurfaceRequestBus() + { + Terrain::TerrainAreaSurfaceRequestBus::Handler::BusDisconnect(); + } + + MOCK_METHOD0(Activate, void()); + MOCK_METHOD0(Deactivate, void()); + MOCK_CONST_METHOD2(GetSurfaceWeights, void(const AZ::Vector3&, AzFramework::SurfaceData::SurfaceTagWeightList&)); + }; + +} // namespace UnitTest diff --git a/Gems/Terrain/Code/Mocks/Terrain/MockTerrainLayerSpawner.h b/Gems/Terrain/Code/Mocks/Terrain/MockTerrainLayerSpawner.h new file mode 100644 index 0000000000..d0551d0881 --- /dev/null +++ b/Gems/Terrain/Code/Mocks/Terrain/MockTerrainLayerSpawner.h @@ -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 + * + */ +#pragma once + +#include + +namespace UnitTest +{ + class MockTerrainLayerSpawnerComponent + : public AZ::Component + { + public: + AZ_COMPONENT(MockTerrainLayerSpawnerComponent, "{9F27C980-9826-4063-86D8-E981C1E842A3}"); + + static void Reflect([[maybe_unused]] AZ::ReflectContext* context) + { + } + + void Activate() override + { + } + + void Deactivate() override + { + } + + private: + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("TerrainAreaService")); + } + }; + +} //namespace UnitTest diff --git a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp index 4ea16018d1..01ad0bb77f 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp @@ -65,7 +65,7 @@ namespace Terrain void TerrainHeightGradientListComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services) { services.push_back(AZ_CRC_CE("TerrainAreaService")); - services.push_back(AZ_CRC_CE("BoxShapeService")); + services.push_back(AZ_CRC_CE("AxisAlignedBoxShapeService")); } void TerrainHeightGradientListComponent::Reflect(AZ::ReflectContext* context) diff --git a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp index 245c98ccac..00ed9c004f 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp @@ -102,7 +102,6 @@ namespace Terrain void TerrainLayerSpawnerComponent::Activate() { - AZ::TransformNotificationBus::Handler::BusConnect(GetEntityId()); LmbrCentral::ShapeComponentNotificationsBus::Handler::BusConnect(GetEntityId()); TerrainSpawnerRequestBus::Handler::BusConnect(GetEntityId()); @@ -114,8 +113,6 @@ namespace Terrain TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::UnregisterArea, GetEntityId()); TerrainSpawnerRequestBus::Handler::BusDisconnect(); LmbrCentral::ShapeComponentNotificationsBus::Handler::BusDisconnect(); - AZ::TransformNotificationBus::Handler::BusDisconnect(); - } bool TerrainLayerSpawnerComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig) @@ -138,13 +135,12 @@ namespace Terrain return false; } - void TerrainLayerSpawnerComponent::OnTransformChanged([[maybe_unused]] const AZ::Transform& local, [[maybe_unused]] const AZ::Transform& world) - { - RefreshArea(); - } - void TerrainLayerSpawnerComponent::OnShapeChanged([[maybe_unused]] ShapeChangeReasons changeReason) { + // This will notify us of both shape changes and transform changes. + // It's important to use this event for transform changes instead of listening to OnTransformChanged, because we need to guarantee + // the shape has received the transform change message and updated its internal state before passing it along to us. + RefreshArea(); } diff --git a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h index c7398bf93e..683f9e9f06 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h +++ b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h @@ -18,7 +18,6 @@ #include #include -#include #include #include @@ -56,7 +55,6 @@ namespace Terrain class TerrainLayerSpawnerComponent : public AZ::Component - , private AZ::TransformNotificationBus::Handler , private LmbrCentral::ShapeComponentNotificationsBus::Handler , private Terrain::TerrainSpawnerRequestBus::Handler { @@ -81,10 +79,6 @@ namespace Terrain bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override; protected: - ////////////////////////////////////////////////////////////////////////// - // AZ::TransformNotificationBus::Handler - void OnTransformChanged(const AZ::Transform& local, const AZ::Transform& world) override; - // ShapeComponentNotificationsBus void OnShapeChanged(ShapeChangeReasons changeReason) override; diff --git a/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp new file mode 100644 index 0000000000..e9c235c1bd --- /dev/null +++ b/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp @@ -0,0 +1,321 @@ +/* + * 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 + +namespace Terrain +{ + void TerrainPhysicsColliderConfig::Reflect(AZ::ReflectContext* context) + { + if (auto serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(1) + ; + + if (auto edit = serialize->GetEditContext()) + { + edit->Class( + "Terrain Physics Collider Component", + "Provides terrain data to a physics collider with configurable surface mappings.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true); + } + } + } + + void TerrainPhysicsColliderComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService")); + } + + void TerrainPhysicsColliderComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService")); + } + + void TerrainPhysicsColliderComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("AxisAlignedBoxShapeService")); + } + + void TerrainPhysicsColliderComponent::Reflect(AZ::ReflectContext* context) + { + TerrainPhysicsColliderConfig::Reflect(context); + + if (auto serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(0) + ->Field("Configuration", &TerrainPhysicsColliderComponent::m_configuration) + ; + } + } + + TerrainPhysicsColliderComponent::TerrainPhysicsColliderComponent(const TerrainPhysicsColliderConfig& configuration) + : m_configuration(configuration) + { + } + + TerrainPhysicsColliderComponent::TerrainPhysicsColliderComponent() + { + + } + + void TerrainPhysicsColliderComponent::Activate() + { + const auto entityId = GetEntityId(); + LmbrCentral::ShapeComponentNotificationsBus::Handler::BusConnect(entityId); + Physics::HeightfieldProviderRequestsBus::Handler::BusConnect(entityId); + AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect(); + + NotifyListenersOfHeightfieldDataChange(); + } + + void TerrainPhysicsColliderComponent::Deactivate() + { + AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect(); + Physics::HeightfieldProviderRequestsBus::Handler ::BusDisconnect(); + LmbrCentral::ShapeComponentNotificationsBus::Handler::BusDisconnect(); + } + + bool TerrainPhysicsColliderComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig) + { + if (auto config = azrtti_cast(baseConfig)) + { + m_configuration = *config; + return true; + } + return false; + } + + bool TerrainPhysicsColliderComponent::WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const + { + if (auto config = azrtti_cast(outBaseConfig)) + { + *config = m_configuration; + return true; + } + return false; + } + + void TerrainPhysicsColliderComponent::NotifyListenersOfHeightfieldDataChange() + { + AZ::Aabb worldSize = AZ::Aabb::CreateNull(); + + LmbrCentral::ShapeComponentRequestsBus::EventResult( + worldSize, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb); + + Physics::HeightfieldProviderNotificationBus::Broadcast( + &Physics::HeightfieldProviderNotificationBus::Events::OnHeightfieldDataChanged, worldSize); + } + + void TerrainPhysicsColliderComponent::OnShapeChanged([[maybe_unused]] ShapeChangeReasons changeReason) + { + // This will notify us of both shape changes and transform changes. + // It's important to use this event for transform changes instead of listening to OnTransformChanged, because we need to guarantee + // the shape has received the transform change message and updated its internal state before passing it along to us. + + NotifyListenersOfHeightfieldDataChange(); + } + + void TerrainPhysicsColliderComponent::OnTerrainDataCreateEnd() + { + // The terrain system has finished creating itself, so we should now have data for creating a heightfield. + NotifyListenersOfHeightfieldDataChange(); + } + + void TerrainPhysicsColliderComponent::OnTerrainDataDestroyBegin() + { + // The terrain system is starting to destroy itself, so notify listeners of a change since the heightfield + // will no longer have any valid data. + NotifyListenersOfHeightfieldDataChange(); + } + + void TerrainPhysicsColliderComponent::OnTerrainDataChanged( + [[maybe_unused]] const AZ::Aabb& dirtyRegion, [[maybe_unused]] TerrainDataChangedMask dataChangedMask) + { + NotifyListenersOfHeightfieldDataChange(); + } + + AZ::Aabb TerrainPhysicsColliderComponent::GetHeightfieldAabb() const + { + AZ::Aabb worldSize = AZ::Aabb::CreateNull(); + + LmbrCentral::ShapeComponentRequestsBus::EventResult( + worldSize, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb); + + auto vector2Floor = [](const AZ::Vector2& in) + { + return AZ::Vector2(floor(in.GetX()), floor(in.GetY())); + }; + auto vector2Ceil = [](const AZ::Vector2& in) + { + return AZ::Vector2(ceil(in.GetX()), ceil(in.GetY())); + }; + + const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing(); + const AZ::Vector3 boundsMin = worldSize.GetMin(); + const AZ::Vector3 boundsMax = worldSize.GetMax(); + + const AZ::Vector2 gridMinBoundLower = vector2Floor(AZ::Vector2(boundsMin) / gridResolution) * gridResolution; + const AZ::Vector2 gridMaxBoundUpper = vector2Ceil(AZ::Vector2(boundsMax) / gridResolution) * gridResolution; + + return AZ::Aabb::CreateFromMinMaxValues( + gridMinBoundLower.GetX(), gridMinBoundLower.GetY(), boundsMin.GetZ(), + gridMaxBoundUpper.GetX(), gridMaxBoundUpper.GetY(), boundsMax.GetZ() + ); + } + + void TerrainPhysicsColliderComponent::GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const + { + AZ::Aabb heightfieldAabb = GetHeightfieldAabb(); + + // Because our terrain heights are relative to the center of the bounding box, the min and max allowable heights are also + // relative to the center. They are also clamped to the size of the bounding box. + minHeightBounds = -(heightfieldAabb.GetZExtent() / 2.0f); + maxHeightBounds = heightfieldAabb.GetZExtent() / 2.0f; + } + + AZ::Transform TerrainPhysicsColliderComponent::GetHeightfieldTransform() const + { + // We currently don't support rotation of terrain heightfields. + AZ::Vector3 translate; + AZ::TransformBus::EventResult(translate, GetEntityId(), &AZ::TransformBus::Events::GetWorldTranslation); + + AZ::Transform transform = AZ::Transform::CreateTranslation(translate); + + return transform; + } + + void TerrainPhysicsColliderComponent::GenerateHeightsInBounds(AZStd::vector& heights) const + { + const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing(); + + AZ::Aabb worldSize = GetHeightfieldAabb(); + + const float worldCenterZ = worldSize.GetCenter().GetZ(); + + int32_t gridWidth, gridHeight; + GetHeightfieldGridSize(gridWidth, gridHeight); + + heights.clear(); + heights.reserve(gridWidth * gridHeight); + + for (int32_t row = 0; row < gridHeight; row++) + { + const float y = row * gridResolution.GetY() + worldSize.GetMin().GetY(); + for (int32_t col = 0; col < gridWidth; col++) + { + const float x = col * gridResolution.GetX() + worldSize.GetMin().GetX(); + float height = 0.0f; + + AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( + height, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats, x, y, + AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT, nullptr); + + heights.emplace_back(height - worldCenterZ); + } + } + } + + void TerrainPhysicsColliderComponent::GenerateHeightsAndMaterialsInBounds( + AZStd::vector& heightMaterials) const + { + const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing(); + + AZ::Aabb worldSize = GetHeightfieldAabb(); + + const float worldCenterZ = worldSize.GetCenter().GetZ(); + const float worldHeightBoundsMin = worldSize.GetMin().GetZ(); + const float worldHeightBoundsMax = worldSize.GetMax().GetZ(); + + int32_t gridWidth, gridHeight; + GetHeightfieldGridSize(gridWidth, gridHeight); + + heightMaterials.clear(); + heightMaterials.reserve(gridWidth * gridHeight); + + for (int32_t row = 0; row < gridHeight; row++) + { + const float y = row * gridResolution.GetY() + worldSize.GetMin().GetY(); + for (int32_t col = 0; col < gridWidth; col++) + { + const float x = col * gridResolution.GetX() + worldSize.GetMin().GetX(); + float height = 0.0f; + + bool terrainExists = true; + AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( + height, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats, x, y, + AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT, &terrainExists); + + // Any heights that fall outside the range of our bounding box will get turned into holes. + if ((height < worldHeightBoundsMin) || (height > worldHeightBoundsMax)) + { + height = worldHeightBoundsMin; + terrainExists = false; + } + + Physics::HeightMaterialPoint point; + point.m_height = height - worldCenterZ; + point.m_quadMeshType = terrainExists ? Physics::QuadMeshType::SubdivideUpperLeftToBottomRight : Physics::QuadMeshType::Hole; + heightMaterials.emplace_back(point); + } + } + } + + AZ::Vector2 TerrainPhysicsColliderComponent::GetHeightfieldGridSpacing() const + { + AZ::Vector2 gridResolution = AZ::Vector2(1.0f); + AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( + gridResolution, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution); + + return gridResolution; + } + + void TerrainPhysicsColliderComponent::GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const + { + const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing(); + const AZ::Aabb bounds = GetHeightfieldAabb(); + + numColumns = aznumeric_cast((bounds.GetMax().GetX() - bounds.GetMin().GetX()) / gridResolution.GetX()); + numRows = aznumeric_cast((bounds.GetMax().GetY() - bounds.GetMin().GetY()) / gridResolution.GetY()); + } + + AZStd::vector TerrainPhysicsColliderComponent::GetMaterialList() const + { + return AZStd::vector(); + } + + AZStd::vector TerrainPhysicsColliderComponent::GetHeights() const + { + AZStd::vector heights; + GenerateHeightsInBounds(heights); + + return heights; + } + + AZStd::vector TerrainPhysicsColliderComponent::GetHeightsAndMaterials() const + { + AZStd::vector heightMaterials; + GenerateHeightsAndMaterialsInBounds(heightMaterials); + + return heightMaterials; + } +} diff --git a/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.h b/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.h new file mode 100644 index 0000000000..e268223689 --- /dev/null +++ b/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.h @@ -0,0 +1,91 @@ +/* + * 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 LmbrCentral +{ + template + class EditorWrappedComponentBase; +} + +namespace Terrain +{ + class TerrainPhysicsColliderConfig + : public AZ::ComponentConfig + { + public: + AZ_CLASS_ALLOCATOR(TerrainPhysicsColliderConfig, AZ::SystemAllocator, 0); + AZ_RTTI(TerrainPhysicsColliderConfig, "{E9EADB8F-C3A5-4B9C-A62D-2DBC86B4CE59}", AZ::ComponentConfig); + static void Reflect(AZ::ReflectContext* context); + + }; + + + class TerrainPhysicsColliderComponent + : public AZ::Component + , public Physics::HeightfieldProviderRequestsBus::Handler + , protected LmbrCentral::ShapeComponentNotificationsBus::Handler + , protected AzFramework::Terrain::TerrainDataNotificationBus::Handler + { + public: + template + friend class LmbrCentral::EditorWrappedComponentBase; + AZ_COMPONENT(TerrainPhysicsColliderComponent, "{33C20287-1D37-44D0-96A0-2C3766E23624}"); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services); + static void Reflect(AZ::ReflectContext* context); + + TerrainPhysicsColliderComponent(const TerrainPhysicsColliderConfig& configuration); + TerrainPhysicsColliderComponent(); + ~TerrainPhysicsColliderComponent() = default; + + // HeightfieldProviderRequestsBus + AZ::Vector2 GetHeightfieldGridSpacing() const override; + void GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const override; + void GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const override; + AZ::Aabb GetHeightfieldAabb() const override; + AZ::Transform GetHeightfieldTransform() const override; + AZStd::vector GetMaterialList() const override; + AZStd::vector GetHeights() const override; + AZStd::vector GetHeightsAndMaterials() const override; + + protected: + ////////////////////////////////////////////////////////////////////////// + // AZ::Component interface implementation + void Activate() override; + void Deactivate() override; + bool ReadInConfig(const AZ::ComponentConfig* baseConfig) override; + bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override; + + void GenerateHeightsInBounds(AZStd::vector& heights) const; + void GenerateHeightsAndMaterialsInBounds(AZStd::vector& heightMaterials) const; + + void NotifyListenersOfHeightfieldDataChange(); + + // ShapeComponentNotificationsBus + void OnShapeChanged(ShapeChangeReasons changeReason) override; + + void OnTerrainDataCreateEnd() override; + void OnTerrainDataDestroyBegin() override; + void OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) override; + + private: + TerrainPhysicsColliderConfig m_configuration; + }; +} diff --git a/Gems/Terrain/Code/Source/Components/TerrainSurfaceDataSystemComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainSurfaceDataSystemComponent.cpp index 0346ff7281..7395e66d06 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainSurfaceDataSystemComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainSurfaceDataSystemComponent.cpp @@ -149,13 +149,17 @@ namespace Terrain if (terrain->GetTerrainAabb().Contains(inPosition)) { bool isTerrainValidAtPoint = false; - const float terrainHeight = terrain->GetHeight(inPosition, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &isTerrainValidAtPoint); + AzFramework::SurfaceData::SurfacePoint terrainSurfacePoint; + terrain->GetSurfacePoint( + inPosition, terrainSurfacePoint, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, + &isTerrainValidAtPoint); + const bool isHole = !isTerrainValidAtPoint; SurfaceData::SurfacePoint point; point.m_entityId = GetEntityId(); - point.m_position = AZ::Vector3(inPosition.GetX(), inPosition.GetY(), terrainHeight); - point.m_normal = terrain->GetNormal(inPosition); + point.m_position = terrainSurfacePoint.m_position; + point.m_normal = terrainSurfacePoint.m_normal; // Always add a "terrain" or "terrainHole" tag. const AZ::Crc32 terrainTag = @@ -163,9 +167,7 @@ namespace Terrain SurfaceData::AddMaxValueForMasks(point.m_masks, terrainTag, 1.0f); // Add all of the surface tags that the terrain has at this point. - AzFramework::SurfaceData::OrderedSurfaceTagWeightSet surfaceWeights; - terrain->GetSurfaceWeights(point.m_position, surfaceWeights); - for (auto& tag : surfaceWeights) + for (auto& tag : terrainSurfacePoint.m_surfaceTags) { SurfaceData::AddMaxValueForMasks(point.m_masks, tag.m_surfaceType, tag.m_weight); } diff --git a/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.cpp index 958e175cf3..5b76f15d74 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.cpp @@ -166,7 +166,7 @@ namespace Terrain void TerrainSurfaceGradientListComponent::GetSurfaceWeights( const AZ::Vector3& inPosition, - AzFramework::SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights) const + AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights) const { outSurfaceWeights.clear(); @@ -178,10 +178,7 @@ namespace Terrain GradientSignal::GradientRequestBus::EventResult(weight, mapping.m_gradientEntityId, &GradientSignal::GradientRequestBus::Events::GetValue, params); - AzFramework::SurfaceData::SurfaceTagWeight tagWeight; - tagWeight.m_surfaceType = mapping.m_surfaceTag; - tagWeight.m_weight = weight; - outSurfaceWeights.emplace(tagWeight); + outSurfaceWeights.emplace_back(mapping.m_surfaceTag, weight); } } diff --git a/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.h b/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.h index bb30029f35..20851c127f 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.h +++ b/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.h @@ -72,7 +72,7 @@ namespace Terrain bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override; // TerrainAreaSurfaceRequestBus - void GetSurfaceWeights(const AZ::Vector3& inPosition, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights) const override; + void GetSurfaceWeights(const AZ::Vector3& inPosition, AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights) const override; private: ////////////////////////////////////////////////////////////////////////// diff --git a/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp new file mode 100644 index 0000000000..6df6c3b440 --- /dev/null +++ b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp @@ -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 + * + */ + +#include +#include +#include + +namespace Terrain +{ + void EditorTerrainPhysicsColliderComponent::Reflect(AZ::ReflectContext* context) + { + // Call ReflectSubClass in EditorWrappedComponentBase to handle all the boilerplate reflection. + BaseClassType::ReflectSubClass( + context, 1, + &LmbrCentral::EditorWrappedComponentBaseVersionConverter + ); + } +} diff --git a/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h new file mode 100644 index 0000000000..d2254161a0 --- /dev/null +++ b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h @@ -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 + * + */ + +#pragma once + +#include +#include +#include + +namespace Terrain +{ + class EditorTerrainPhysicsColliderComponent + : public LmbrCentral::EditorWrappedComponentBase + { + public: + using BaseClassType = LmbrCentral::EditorWrappedComponentBase; + AZ_EDITOR_COMPONENT(EditorTerrainPhysicsColliderComponent, "{C43FAB8F-3968-46A6-920E-E84AEDED3DF5}", BaseClassType); + static void Reflect(AZ::ReflectContext* context); + + static constexpr auto s_categoryName = "Terrain"; + static constexpr auto s_componentName = "Terrain Physics Heightfield Collider"; + static constexpr auto s_componentDescription = "Provides terrain data to a physics collider in the form of a heightfield and surface->material mapping."; + static constexpr auto s_icon = "Editor/Icons/Components/TerrainLayerSpawner.svg"; + static constexpr auto s_viewportIcon = "Editor/Icons/Components/Viewport/TerrainLayerSpawner.svg"; + static constexpr auto s_helpUrl = ""; + }; +} diff --git a/Gems/Terrain/Code/Source/EditorTerrainModule.cpp b/Gems/Terrain/Code/Source/EditorTerrainModule.cpp index 6fd8979e4a..0ea822e8b5 100644 --- a/Gems/Terrain/Code/Source/EditorTerrainModule.cpp +++ b/Gems/Terrain/Code/Source/EditorTerrainModule.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,7 @@ namespace Terrain Terrain::EditorTerrainSurfaceMaterialsListComponent::CreateDescriptor(), Terrain::EditorTerrainWorldComponent::CreateDescriptor(), Terrain::EditorTerrainWorldDebuggerComponent::CreateDescriptor(), + Terrain::EditorTerrainPhysicsColliderComponent::CreateDescriptor(), Terrain::EditorTerrainWorldRendererComponent::CreateDescriptor(), }); diff --git a/Gems/Terrain/Code/Source/TerrainModule.cpp b/Gems/Terrain/Code/Source/TerrainModule.cpp index c3a7890565..2524f3684c 100644 --- a/Gems/Terrain/Code/Source/TerrainModule.cpp +++ b/Gems/Terrain/Code/Source/TerrainModule.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -37,6 +38,7 @@ namespace Terrain TerrainMacroMaterialComponent::CreateDescriptor(), TerrainSurfaceGradientListComponent::CreateDescriptor(), TerrainSurfaceDataSystemComponent::CreateDescriptor(), + TerrainPhysicsColliderComponent::CreateDescriptor() }); } diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp index 821e0bd2e2..fa1d483a5d 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp @@ -241,7 +241,12 @@ float TerrainSystem::GetTerrainAreaHeight(float x, float y, bool& terrainExists) return height; } -float TerrainSystem::GetHeight(AZ::Vector3 position, Sampler sampler, bool* terrainExistsPtr) const +float TerrainSystem::GetHeight(const AZ::Vector3& position, Sampler sampler, bool* terrainExistsPtr) const +{ + return GetHeightSynchronous(position.GetX(), position.GetY(), sampler, terrainExistsPtr); +} + +float TerrainSystem::GetHeightFromVector2(const AZ::Vector2& position, Sampler sampler, bool* terrainExistsPtr) const { return GetHeightSynchronous(position.GetX(), position.GetY(), sampler, terrainExistsPtr); } @@ -251,6 +256,20 @@ float TerrainSystem::GetHeightFromFloats(float x, float y, Sampler sampler, bool return GetHeightSynchronous(x, y, sampler, terrainExistsPtr); } +bool TerrainSystem::GetIsHole(const AZ::Vector3& position, Sampler sampler) const +{ + bool terrainExists = false; + GetHeightSynchronous(position.GetX(), position.GetY(), sampler, &terrainExists); + return !terrainExists; +} + +bool TerrainSystem::GetIsHoleFromVector2(const AZ::Vector2& position, Sampler sampler) const +{ + bool terrainExists = false; + GetHeightSynchronous(position.GetX(), position.GetY(), sampler, &terrainExists); + return !terrainExists; +} + bool TerrainSystem::GetIsHoleFromFloats(float x, float y, Sampler sampler) const { bool terrainExists = false; @@ -287,7 +306,12 @@ AZ::Vector3 TerrainSystem::GetNormalSynchronous(float x, float y, Sampler sample return outNormal; } -AZ::Vector3 TerrainSystem::GetNormal(AZ::Vector3 position, Sampler sampler, bool* terrainExistsPtr) const +AZ::Vector3 TerrainSystem::GetNormal(const AZ::Vector3& position, Sampler sampler, bool* terrainExistsPtr) const +{ + return GetNormalSynchronous(position.GetX(), position.GetY(), sampler, terrainExistsPtr); +} + +AZ::Vector3 TerrainSystem::GetNormalFromVector2(const AZ::Vector2& position, Sampler sampler, bool* terrainExistsPtr) const { return GetNormalSynchronous(position.GetX(), position.GetY(), sampler, terrainExistsPtr); } @@ -299,7 +323,7 @@ AZ::Vector3 TerrainSystem::GetNormalFromFloats(float x, float y, Sampler sampler AzFramework::SurfaceData::SurfaceTagWeight TerrainSystem::GetMaxSurfaceWeight( - const AZ::Vector3 position, Sampler sampleFilter, bool* terrainExistsPtr) const + const AZ::Vector3& position, Sampler sampleFilter, bool* terrainExistsPtr) const { return GetMaxSurfaceWeightFromFloats(position.GetX(), position.GetY(), sampleFilter, terrainExistsPtr); } @@ -317,7 +341,7 @@ AzFramework::SurfaceData::SurfaceTagWeight TerrainSystem::GetMaxSurfaceWeightFro *terrainExistsPtr = true; } - AzFramework::SurfaceData::OrderedSurfaceTagWeightSet weightSet; + AzFramework::SurfaceData::SurfaceTagWeightList weightSet; GetOrderedSurfaceWeights(x, y, sampleFilter, weightSet, terrainExistsPtr); @@ -329,6 +353,38 @@ AzFramework::SurfaceData::SurfaceTagWeight TerrainSystem::GetMaxSurfaceWeightFro return *weightSet.begin(); } +void TerrainSystem::GetSurfacePoint( + const AZ::Vector3& inPosition, + AzFramework::SurfaceData::SurfacePoint& outSurfacePoint, + Sampler sampleFilter, + bool* terrainExistsPtr) const +{ + outSurfacePoint.m_position = inPosition; + outSurfacePoint.m_position.SetZ(GetHeightSynchronous(inPosition.GetX(), inPosition.GetY(), sampleFilter, terrainExistsPtr)); + outSurfacePoint.m_normal = GetNormalSynchronous(inPosition.GetX(), inPosition.GetY(), sampleFilter, nullptr); + GetSurfaceWeights(inPosition, outSurfacePoint.m_surfaceTags, sampleFilter, nullptr); +} + +void TerrainSystem::GetSurfacePointFromVector2( + const AZ::Vector2& inPosition, + AzFramework::SurfaceData::SurfacePoint& outSurfacePoint, + Sampler sampleFilter, + bool* terrainExistsPtr) const +{ + GetSurfacePoint(AZ::Vector3(inPosition.GetX(), inPosition.GetY(), 0.0f), outSurfacePoint, sampleFilter, terrainExistsPtr); +} + +void TerrainSystem::GetSurfacePointFromFloats( + float x, + float y, + AzFramework::SurfaceData::SurfacePoint& outSurfacePoint, + Sampler sampleFilter, + bool* terrainExistsPtr) const +{ + GetSurfacePoint(AZ::Vector3(x, y, 0.0f), outSurfacePoint, sampleFilter, terrainExistsPtr); +} + + AZ::EntityId TerrainSystem::FindBestAreaEntityAtPosition(float x, float y, AZ::Aabb& bounds) const { AZ::Vector3 inPosition = AZ::Vector3(x, y, 0); @@ -354,7 +410,7 @@ void TerrainSystem::GetOrderedSurfaceWeights( const float x, const float y, [[maybe_unused]] Sampler sampler, - AzFramework::SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights, + AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights, bool* terrainExistsPtr) const { AZ::Aabb bounds; @@ -377,54 +433,40 @@ void TerrainSystem::GetOrderedSurfaceWeights( // Get all the surfaces with weights at the given point. Terrain::TerrainAreaSurfaceRequestBus::Event( bestAreaId, &Terrain::TerrainAreaSurfaceRequestBus::Events::GetSurfaceWeights, inPosition, outSurfaceWeights); + + AZStd::sort(outSurfaceWeights.begin(), outSurfaceWeights.end(), AzFramework::SurfaceData::SurfaceTagWeightComparator()); } void TerrainSystem::GetSurfaceWeights( const AZ::Vector3& inPosition, - AzFramework::SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights, + AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights, Sampler sampleFilter, bool* terrainExistsPtr) const { - if (terrainExistsPtr) - { - *terrainExistsPtr = true; - } - GetOrderedSurfaceWeights(inPosition.GetX(), inPosition.GetY(), sampleFilter, outSurfaceWeights, terrainExistsPtr); } void TerrainSystem::GetSurfaceWeightsFromVector2( const AZ::Vector2& inPosition, - AzFramework::SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights, + AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights, Sampler sampleFilter, bool* terrainExistsPtr) const { - // For now, always set terrainExists to true, as we don't have a way to author data for terrain holes yet. - if (terrainExistsPtr) - { - *terrainExistsPtr = true; - } GetOrderedSurfaceWeights(inPosition.GetX(), inPosition.GetY(), sampleFilter, outSurfaceWeights, terrainExistsPtr); } void TerrainSystem::GetSurfaceWeightsFromFloats( - float x, - float y, - AzFramework::SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights, + float x, float y, + AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights, Sampler sampleFilter, bool* terrainExistsPtr) const { - // For now, always set terrainExists to true, as we don't have a way to author data for terrain holes yet. - if (terrainExistsPtr) - { - *terrainExistsPtr = true; - } - GetOrderedSurfaceWeights(x, y, sampleFilter, outSurfaceWeights, terrainExistsPtr); } -const char* TerrainSystem::GetMaxSurfaceName([[maybe_unused]] AZ::Vector3 position, [[maybe_unused]] Sampler sampleFilter, [[maybe_unused]] bool* terrainExistsPtr) const +const char* TerrainSystem::GetMaxSurfaceName( + [[maybe_unused]] const AZ::Vector3& position, [[maybe_unused]] Sampler sampleFilter, [[maybe_unused]] bool* terrainExistsPtr) const { // For now, always set terrainExists to true, as we don't have a way to author data for terrain holes yet. if (terrainExistsPtr) @@ -436,21 +478,6 @@ const char* TerrainSystem::GetMaxSurfaceName([[maybe_unused]] AZ::Vector3 positi } /* -void TerrainSystem::GetSurfacePoint( - const AZ::Vector3& inPosition, [[maybe_unused]] Sampler sampleFilter, SurfaceData::SurfacePoint& outSurfacePoint) -{ - // TODO: Handle sampleFilter - - float sampleX = inPosition.GetX(); - float sampleY = inPosition.GetY(); - - GetHeight(inPosition, sampleFilter, outSurfacePoint.m_position); - //outSurfacePoint.m_position = AZ::Vector3(sampleX, sampleY, GetHeightSynchronous(sampleX, sampleY)); - outSurfacePoint.m_normal = GetNormalSynchronous(sampleX, sampleY); -} - - - void TerrainSystem::ProcessHeightsFromRegion(const AZ::Aabb& inRegion, const AZ::Vector2 stepSize, Sampler sampleFilter, SurfacePointRegionFillCallback perPositionCallback, TerrainDataReadyCallback onComplete) { diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h index 66c48850ef..956424f048 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h @@ -62,7 +62,8 @@ namespace Terrain //! @terrainExistsPtr: Can be nullptr. If != nullptr then, if there's no terrain at location x,y or location x,y is inside a terrain //! HOLE then *terrainExistsPtr will become false, //! otherwise *terrainExistsPtr will become true. - float GetHeight(AZ::Vector3 position, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const override; + float GetHeight(const AZ::Vector3& position, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const override; + float GetHeightFromVector2(const AZ::Vector2& position, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const override; float GetHeightFromFloats(float x, float y, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const override; //! Given an XY coordinate, return the max surface type and weight. @@ -70,7 +71,7 @@ namespace Terrain //! HOLE then *terrainExistsPtr will be set to false, //! otherwise *terrainExistsPtr will be set to true. AzFramework::SurfaceData::SurfaceTagWeight GetMaxSurfaceWeight( - const AZ::Vector3 position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const override; + const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const override; AzFramework::SurfaceData::SurfaceTagWeight GetMaxSurfaceWeightFromVector2( const AZ::Vector2& inPosition, Sampler sampleFilter = Sampler::DEFAULT, bool* terrainExistsPtr = nullptr) const override; AzFramework::SurfaceData::SurfaceTagWeight GetMaxSurfaceWeightFromFloats( @@ -78,18 +79,18 @@ namespace Terrain void GetSurfaceWeights( const AZ::Vector3& inPosition, - AzFramework::SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights, + AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights, Sampler sampleFilter = Sampler::DEFAULT, bool* terrainExistsPtr = nullptr) const override; void GetSurfaceWeightsFromVector2( const AZ::Vector2& inPosition, - AzFramework::SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights, + AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights, Sampler sampleFilter = Sampler::DEFAULT, bool* terrainExistsPtr = nullptr) const override; void GetSurfaceWeightsFromFloats( float x, float y, - AzFramework::SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights, + AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights, Sampler sampleFilter = Sampler::DEFAULT, bool* terrainExistsPtr = nullptr) const override; @@ -97,10 +98,12 @@ namespace Terrain //! GetMaxSurfaceWeight or GetMaxSurfaceWeightFromFloats. Not available in the behavior context. Returns nullptr if the position is //! inside a hole or outside of the terrain boundaries. const char* GetMaxSurfaceName( - AZ::Vector3 position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const override; + const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const override; //! Returns true if there's a hole at location x,y. //! Also returns true if there's no terrain data at location x,y. + bool GetIsHole(const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR) const override; + bool GetIsHoleFromVector2(const AZ::Vector2& position, Sampler sampleFilter = Sampler::BILINEAR) const override; bool GetIsHoleFromFloats(float x, float y, Sampler sampleFilter = Sampler::BILINEAR) const override; // Given an XY coordinate, return the surface normal. @@ -108,10 +111,30 @@ namespace Terrain //! HOLE then *terrainExistsPtr will be set to false, //! otherwise *terrainExistsPtr will be set to true. AZ::Vector3 GetNormal( - AZ::Vector3 position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const override; + const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const override; + AZ::Vector3 GetNormalFromVector2( + const AZ::Vector2& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const override; AZ::Vector3 GetNormalFromFloats( float x, float y, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const override; + void GetSurfacePoint( + const AZ::Vector3& inPosition, + AzFramework::SurfaceData::SurfacePoint& outSurfacePoint, + Sampler sampleFilter = Sampler::DEFAULT, + bool* terrainExistsPtr = nullptr) const override; + void GetSurfacePointFromVector2( + const AZ::Vector2& inPosition, + AzFramework::SurfaceData::SurfacePoint& outSurfacePoint, + Sampler sampleFilter = Sampler::DEFAULT, + bool* terrainExistsPtr = nullptr) const override; + void GetSurfacePointFromFloats( + float x, + float y, + AzFramework::SurfaceData::SurfacePoint& outSurfacePoint, + Sampler sampleFilter = Sampler::DEFAULT, + bool* terrainExistsPtr = nullptr) const override; + + private: void ClampPosition(float x, float y, AZ::Vector2& outPosition, AZ::Vector2& normalizedDelta) const; @@ -120,11 +143,11 @@ namespace Terrain const float x, const float y, Sampler sampler, - AzFramework::SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights, + AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights, bool* terrainExistsPtr) const; float GetHeightSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const; float GetTerrainAreaHeight(float x, float y, bool& terrainExists) const; - AZ::Vector3 GetNormalSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const; + AZ::Vector3 GetNormalSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const; // AZ::TickBus::Handler overrides ... void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; diff --git a/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp b/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp index ee4b18e2fb..0ca5e7d99a 100644 --- a/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp +++ b/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp @@ -7,6 +7,7 @@ */ #include +#include #include #include @@ -195,8 +196,10 @@ TEST_F(LayerSpawnerComponentTest, LayerSpawnerTransformChangedUpdatesTerrainSyst m_entity->Activate(); - AZ::TransformNotificationBus::Event( - m_entity->GetId(), &AZ::TransformNotificationBus::Events::OnTransformChanged, AZ::Transform(), AZ::Transform()); + // The component gets transform change notifications via the shape bus. + LmbrCentral::ShapeComponentNotificationsBus::Event( + m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged, + LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::TransformChanged); m_entity->Deactivate(); } diff --git a/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp b/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp new file mode 100644 index 0000000000..c314e4e968 --- /dev/null +++ b/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp @@ -0,0 +1,146 @@ +/* + * 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 + +using ::testing::_; +using ::testing::AtLeast; +using ::testing::Mock; +using ::testing::NiceMock; +using ::testing::Return; + +class TerrainHeightGradientListComponentTest : public ::testing::Test +{ +protected: + AZ::ComponentApplication m_app; + + AZStd::unique_ptr m_entity; + + void SetUp() override + { + AZ::ComponentApplication::Descriptor appDesc; + appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; + appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; + appDesc.m_stackRecordLevels = 20; + + m_app.Create(appDesc); + } + + void TearDown() override + { + m_app.Destroy(); + } + + void CreateEntity() + { + m_entity = AZStd::make_unique(); + ASSERT_TRUE(m_entity); + + // Create the required box component. + UnitTest::MockAxisAlignedBoxShapeComponent* boxComponent = m_entity->CreateComponent(); + m_app.RegisterComponentDescriptor(boxComponent->CreateDescriptor()); + + // Create the TerrainHeightGradientListComponent with an entity in its configuration. + Terrain::TerrainHeightGradientListConfig config; + config.m_gradientEntities.push_back(m_entity->GetId()); + + Terrain::TerrainHeightGradientListComponent* heightGradientListComponent = m_entity->CreateComponent(config); + m_app.RegisterComponentDescriptor(heightGradientListComponent->CreateDescriptor()); + + // Create a MockTerrainLayerSpawnerComponent to provide the required TerrainAreaService. + UnitTest::MockTerrainLayerSpawnerComponent* layerSpawner = m_entity->CreateComponent(); + m_app.RegisterComponentDescriptor(layerSpawner->CreateDescriptor()); + + m_entity->Init(); + } +}; + +TEST_F(TerrainHeightGradientListComponentTest, ActivateEntityActivateSuccess) +{ + // Check that the entity activates. + CreateEntity(); + + m_entity->Activate(); + EXPECT_EQ(m_entity->GetState(), AZ::Entity::State::Active); + + m_entity.reset(); +} + +TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientRefreshesTerrainSystem) +{ + // Check that the HeightGradientListComponent informs the TerrainSystem when the composition changes. + CreateEntity(); + + m_entity->Activate(); + + NiceMock terrainSystem; + + // As the TerrainHeightGradientListComponent subscribes to the dependency monitor, RefreshArea will be called twice: + // once due to OnCompositionChanged being picked up by the the dependency monitor and resending the notification, + // and once when the HeightGradientListComponent gets the OnCompositionChanged directly through the DependencyNotificationBus. + EXPECT_CALL(terrainSystem, RefreshArea(_)).Times(2); + + LmbrCentral::DependencyNotificationBus::Event(m_entity->GetId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged); + + // Stop the EXPECT_CALL check now, as OnCompositionChanged will get called twice again during the reset. + Mock::VerifyAndClearExpectations(&terrainSystem); + + m_entity.reset(); +} + +TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientListReturnsHeights) +{ + // Check that the HeightGradientListComponent returns expected height values. + CreateEntity(); + + NiceMock heightfieldRequestBus(m_entity->GetId()); + + m_entity->Activate(); + + const float mockGradientValue = 0.25f; + NiceMock gradientRequests(m_entity->GetId()); + ON_CALL(gradientRequests, GetValue).WillByDefault(Return(mockGradientValue)); + + // Setup a mock to provide the encompassing Aabb to the HeightGradientListComponent. + const float min = 0.0f; + const float max = 1000.0f; + const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(min), AZ::Vector3(max)); + NiceMock mockShapeRequests(m_entity->GetId()); + ON_CALL(mockShapeRequests, GetEncompassingAabb).WillByDefault(Return(aabb)); + + const float worldMax = 10000.0f; + const AZ::Aabb worldAabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(min), AZ::Vector3(worldMax)); + NiceMock mockterrainDataRequests; + ON_CALL(mockterrainDataRequests, GetTerrainHeightQueryResolution).WillByDefault(Return(AZ::Vector2(1.0f))); + ON_CALL(mockterrainDataRequests, GetTerrainAabb).WillByDefault(Return(worldAabb)); + + // Ensure the cached values in the HeightGradientListComponent are up to date. + LmbrCentral::DependencyNotificationBus::Event(m_entity->GetId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged); + + const AZ::Vector3 inPosition = AZ::Vector3::CreateZero(); + AZ::Vector3 outPosition = AZ::Vector3::CreateZero(); + bool terrainExists = false; + Terrain::TerrainAreaHeightRequestBus::Event(m_entity->GetId(), &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, terrainExists); + + const float height = outPosition.GetZ(); + + EXPECT_NEAR(height, mockGradientValue * max, 0.01f); + + m_entity.reset(); +} + diff --git a/Gems/Terrain/Code/Tests/TerrainPhysicsColliderTests.cpp b/Gems/Terrain/Code/Tests/TerrainPhysicsColliderTests.cpp new file mode 100644 index 0000000000..d1deca897c --- /dev/null +++ b/Gems/Terrain/Code/Tests/TerrainPhysicsColliderTests.cpp @@ -0,0 +1,292 @@ +/* + * 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 + +using ::testing::NiceMock; +using ::testing::AtLeast; +using ::testing::_; +using ::testing::Return; + +class TerrainPhysicsColliderComponentTest + : public ::testing::Test +{ +protected: + AZ::ComponentApplication m_app; + + AZStd::unique_ptr m_entity; + Terrain::TerrainPhysicsColliderComponent* m_colliderComponent; + UnitTest::MockAxisAlignedBoxShapeComponent* m_boxComponent; + + void SetUp() override + { + AZ::ComponentApplication::Descriptor appDesc; + appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; + appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; + appDesc.m_stackRecordLevels = 20; + + m_app.Create(appDesc); + } + + void TearDown() override + { + m_app.Destroy(); + } + + void CreateEntity() + { + m_entity = AZStd::make_unique(); + ASSERT_TRUE(m_entity); + + m_entity->Init(); + } + + void AddTerrainPhysicsColliderAndShapeComponentToEntity() + { + m_boxComponent = m_entity->CreateComponent(); + m_app.RegisterComponentDescriptor(m_boxComponent->CreateDescriptor()); + + m_colliderComponent = m_entity->CreateComponent(Terrain::TerrainPhysicsColliderConfig()); + m_app.RegisterComponentDescriptor(m_colliderComponent->CreateDescriptor()); + } +}; + +TEST_F(TerrainPhysicsColliderComponentTest, ActivateEntityActivateSuccess) +{ + // Check that the entity activates with a collider and the required shape attached. + CreateEntity(); + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + EXPECT_EQ(m_entity->GetState(), AZ::Entity::State::Active); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderTransformChangedNotifiesHeightfieldBus) +{ + // Check that the HeightfieldBus is notified when the transform of the entity changes. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + NiceMock heightfieldListener(m_entity->GetId()); + EXPECT_CALL(heightfieldListener, OnHeightfieldDataChanged(_)).Times(1); + + // The component gets transform change notifications via the shape bus. + LmbrCentral::ShapeComponentNotificationsBus::Event( + m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged, + LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::TransformChanged); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderShapeChangedNotifiesHeightfieldBus) +{ + // Check that the Heightfield bus is notified when the shape component changes. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + NiceMock heightfieldListener(m_entity->GetId()); + EXPECT_CALL(heightfieldListener, OnHeightfieldDataChanged(_)).Times(1); + + LmbrCentral::ShapeComponentNotificationsBus::Event( + m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged, + LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderReturnsAlignedRowBoundsCorrectly) +{ + // Check that the heightfield grid size is correct when the shape bounds match the grid resolution. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const float boundsMin = 0.0f; + const float boundsMax = 1024.0f; + + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + const AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + NiceMock terrainListener; + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + int32_t cols, rows; + Physics::HeightfieldProviderRequestsBus::Event( + m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows); + + // With the bounds set at 0-1024 and a resolution of 1.0, the heightfield grid should be 1024x1024. + EXPECT_EQ(cols, 1024); + EXPECT_EQ(rows, 1024); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderExpandsMinBoundsCorrectly) +{ + // Check that the heightfield grid is correctly expanded if the minimum value of the bounds needs expanding + // to correctly encompass it. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const float boundsMin = 0.1f; + const float boundsMax = 1024.0f; + + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + NiceMock terrainListener; + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + int32_t cols, rows; + Physics::HeightfieldProviderRequestsBus::Event( + m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows); + + // If the heightfield is not expanded to ensure it encompasses the shape bounds, + // the values returned would be 1023. + EXPECT_EQ(cols, 1024); + EXPECT_EQ(rows, 1024); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderExpandsMaxBoundsCorrectly) +{ + // Check that the heightfield grid is correctly expanded if the maximum value of the bounds needs expanding + // to correctly encompass it. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const float boundsMin = 0.0f; + const float boundsMax = 1023.5f; + + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + NiceMock terrainListener; + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + int32_t cols, rows; + Physics::HeightfieldProviderRequestsBus::Event( + m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows); + + // If the heightfield is not expanded to ensure it encompasses the shape bounds, + // the values returned would be 1023. + EXPECT_EQ(cols, 1024); + EXPECT_EQ(rows, 1024); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderGetHeightsReturnsHeights) +{ + // Check that the TerrainPhysicsCollider returns a heightfield of the expected size. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const float boundsMin = 0.0f; + const float boundsMax = 1024.0f; + + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + NiceMock terrainListener; + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + int32_t cols, rows; + Physics::HeightfieldProviderRequestsBus::Event( + m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows); + + AZStd::vector heights; + + Physics::HeightfieldProviderRequestsBus::EventResult( + heights, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeights); + + EXPECT_EQ(cols, 1024); + EXPECT_EQ(rows, 1024); + EXPECT_EQ(heights.size(), cols * rows); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderReturnsRelativeHeightsCorrectly) +{ + // Check that the values stored in the heightfield returned by the TerrainPhysicsCollider are correct. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const AZ::Vector3 boundsMin = AZ::Vector3(0.0f); + const AZ::Vector3 boundsMax = AZ::Vector3(256.0f, 256.0f, 32768.0f); + + const float mockHeight = 32768.0f; + AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + + NiceMock terrainListener; + ON_CALL(terrainListener, GetHeightFromFloats).WillByDefault(Return(mockHeight)); + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + // Just return the bounds as setup. This is equivalent to the box being at the origin. + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + AZStd::vector heights; + + Physics::HeightfieldProviderRequestsBus::EventResult(heights, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeights); + + ASSERT_FALSE(heights.empty()); + + const float expectedHeightValue = 16384.0f; + EXPECT_NEAR(heights[0], expectedHeightValue, 0.01f); + + m_entity->Reset(); +} diff --git a/Gems/Terrain/Code/Tests/TerrainSurfaceGradientListTests.cpp b/Gems/Terrain/Code/Tests/TerrainSurfaceGradientListTests.cpp new file mode 100644 index 0000000000..dea861bda5 --- /dev/null +++ b/Gems/Terrain/Code/Tests/TerrainSurfaceGradientListTests.cpp @@ -0,0 +1,129 @@ +/* + * 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 + +using ::testing::NiceMock; +using ::testing::AtLeast; +using ::testing::_; +using ::testing::Return; + +namespace UnitTest +{ + class TerrainSurfaceGradientListTest : public ::testing::Test + { + protected: + AZ::ComponentApplication m_app; + + AZStd::unique_ptr m_entity; + UnitTest::MockTerrainLayerSpawnerComponent* m_layerSpawnerComponent = nullptr; + AZStd::unique_ptr m_gradientEntity1, m_gradientEntity2; + + const AZStd::string surfaceTag1 = "testtag1"; + const AZStd::string surfaceTag2 = "testtag2"; + + void SetUp() override + { + AZ::ComponentApplication::Descriptor appDesc; + appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; + appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; + appDesc.m_stackRecordLevels = 20; + + m_app.Create(appDesc); + + CreateEntities(); + } + + void TearDown() override + { + m_gradientEntity2.reset(); + m_gradientEntity1.reset(); + m_entity.reset(); + + m_app.Destroy(); + } + + void CreateEntities() + { + m_entity = AZStd::make_unique(); + ASSERT_TRUE(m_entity); + + m_entity->Init(); + + m_gradientEntity1 = AZStd::make_unique(); + ASSERT_TRUE(m_gradientEntity1); + + m_gradientEntity1->Init(); + + m_gradientEntity2 = AZStd::make_unique(); + ASSERT_TRUE(m_gradientEntity2); + + m_gradientEntity2->Init(); + } + + void AddSurfaceGradientListToEntities() + { + m_layerSpawnerComponent = m_entity->CreateComponent(); + m_app.RegisterComponentDescriptor(m_layerSpawnerComponent->CreateDescriptor()); + + Terrain::TerrainSurfaceGradientListConfig config; + + Terrain::TerrainSurfaceGradientMapping mapping1; + mapping1.m_gradientEntityId = m_gradientEntity1->GetId(); + mapping1.m_surfaceTag = SurfaceData::SurfaceTag(surfaceTag1); + config.m_gradientSurfaceMappings.emplace_back(mapping1); + + Terrain::TerrainSurfaceGradientMapping mapping2; + mapping2.m_gradientEntityId = m_gradientEntity2->GetId(); + mapping2.m_surfaceTag = SurfaceData::SurfaceTag(surfaceTag2); + config.m_gradientSurfaceMappings.emplace_back(mapping2); + + Terrain::TerrainSurfaceGradientListComponent* terrainSurfaceGradientListComponent = + m_entity->CreateComponent(config); + m_app.RegisterComponentDescriptor(terrainSurfaceGradientListComponent->CreateDescriptor()); + } + }; + + TEST_F(TerrainSurfaceGradientListTest, SurfaceGradientReturnsSurfaceWeights) + { + // When there is more than one surface/weight defined and added to the component, they should all + // be returned. The component isn't required to return them in descending order. + AddSurfaceGradientListToEntities(); + + m_entity->Activate(); + m_gradientEntity1->Activate(); + m_gradientEntity2->Activate(); + + const float gradient1Value = 0.3f; + NiceMock mockGradientRequests1(m_gradientEntity1->GetId()); + ON_CALL(mockGradientRequests1, GetValue).WillByDefault(Return(gradient1Value)); + + const float gradient2Value = 1.0f; + NiceMock mockGradientRequests2(m_gradientEntity2->GetId()); + ON_CALL(mockGradientRequests2, GetValue).WillByDefault(Return(gradient2Value)); + + AzFramework::SurfaceData::SurfaceTagWeightList weightList; + Terrain::TerrainAreaSurfaceRequestBus::Event( + m_entity->GetId(), &Terrain::TerrainAreaSurfaceRequestBus::Events::GetSurfaceWeights, AZ::Vector3::CreateZero(), weightList); + + AZ::Crc32 expectedCrcList[] = { AZ::Crc32(surfaceTag1), AZ::Crc32(surfaceTag2) }; + const float expectedWeightList[] = { gradient1Value, gradient2Value }; + + int index = 0; + for (const auto& surfaceWeight : weightList) + { + EXPECT_EQ(surfaceWeight.m_surfaceType, expectedCrcList[index]); + EXPECT_NEAR(surfaceWeight.m_weight, expectedWeightList[index], 0.01f); + index++; + } + } +} // namespace UnitTest + + diff --git a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp index e4fb0f348e..4d149e8527 100644 --- a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp +++ b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp @@ -13,9 +13,10 @@ #include #include -#include +#include #include +#include #include using ::testing::AtLeast; @@ -25,451 +26,550 @@ using ::testing::IsFalse; using ::testing::Ne; using ::testing::NiceMock; using ::testing::Return; +using ::testing::SetArgReferee; -class TerrainSystemTest : public ::testing::Test +namespace UnitTest { -protected: - // Defines a structure for defining both an XY position and the expected height for that position. - struct HeightTestPoint + class TerrainSystemTest : public ::testing::Test { - AZ::Vector2 m_testLocation; - float m_expectedHeight; - }; + protected: + // Defines a structure for defining both an XY position and the expected height for that position. + struct HeightTestPoint + { + AZ::Vector2 m_testLocation = AZ::Vector2::CreateZero(); + float m_expectedHeight = 0.0f; + }; - AZ::ComponentApplication m_app; - AZStd::unique_ptr m_terrainSystem; + AZ::ComponentApplication m_app; + AZStd::unique_ptr m_terrainSystem; - AZStd::unique_ptr> m_boxShapeRequests; - AZStd::unique_ptr> m_shapeRequests; - AZStd::unique_ptr> m_terrainAreaHeightRequests; + AZStd::unique_ptr> m_boxShapeRequests; + AZStd::unique_ptr> m_shapeRequests; + AZStd::unique_ptr> m_terrainAreaHeightRequests; + void SetUp() override + { + AZ::ComponentApplication::Descriptor appDesc; + appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; + appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; + appDesc.m_stackRecordLevels = 20; - void SetUp() override - { - AZ::ComponentApplication::Descriptor appDesc; - appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; - appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; - appDesc.m_stackRecordLevels = 20; + m_app.Create(appDesc); + } - m_app.Create(appDesc); - } + void TearDown() override + { + m_terrainSystem.reset(); + m_boxShapeRequests.reset(); + m_shapeRequests.reset(); + m_terrainAreaHeightRequests.reset(); + m_app.Destroy(); + } - void TearDown() override - { - m_terrainSystem.reset(); - m_boxShapeRequests.reset(); - m_shapeRequests.reset(); - m_terrainAreaHeightRequests.reset(); - m_app.Destroy(); - } + AZStd::unique_ptr CreateEntity() + { + return AZStd::make_unique(); + } - AZStd::unique_ptr CreateEntity() - { - return AZStd::make_unique(); - } + void ActivateEntity(AZ::Entity* entity) + { + entity->Init(); + EXPECT_EQ(AZ::Entity::State::Init, entity->GetState()); - void ActivateEntity(AZ::Entity* entity) - { - entity->Init(); - EXPECT_EQ(AZ::Entity::State::Init, entity->GetState()); + entity->Activate(); + EXPECT_EQ(AZ::Entity::State::Active, entity->GetState()); + } - entity->Activate(); - EXPECT_EQ(AZ::Entity::State::Active, entity->GetState()); - } + template + AZ::Component* CreateComponent(AZ::Entity* entity, const Configuration& config) + { + m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); + return entity->CreateComponent(config); + } - template - AZ::Component* CreateComponent(AZ::Entity* entity, const Configuration& config) - { - m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); - return entity->CreateComponent(config); - } + template + AZ::Component* CreateComponent(AZ::Entity* entity) + { + m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); + return entity->CreateComponent(); + } + + // Create a terrain system with reasonable defaults for testing, but with the ability to override the defaults + // on a test-by-test basis. + void CreateAndActivateTerrainSystem( + AZ::Vector2 queryResolution = AZ::Vector2(1.0f), + AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-128.0f), AZ::Vector3(128.0f))) + { + // Create the terrain system and give it one tick to fully initialize itself. + m_terrainSystem = AZStd::make_unique(); + m_terrainSystem->SetTerrainAabb(worldBounds); + m_terrainSystem->SetTerrainHeightQueryResolution(queryResolution); + m_terrainSystem->Activate(); + AZ::TickBus::Broadcast(&AZ::TickBus::Events::OnTick, 0.f, AZ::ScriptTimePoint{}); + } + + AZStd::unique_ptr CreateAndActivateMockTerrainLayerSpawner( + const AZ::Aabb& spawnerBox, const AZStd::function& mockHeights) + { + // Create the base entity with a mock box shape, Terrain Layer Spawner, and height provider. + auto entity = CreateEntity(); + CreateComponent(entity.get()); + CreateComponent(entity.get()); + + m_boxShapeRequests = AZStd::make_unique>(entity->GetId()); + m_shapeRequests = AZStd::make_unique>(entity->GetId()); + + // Set up the box shape to return whatever spawnerBox was passed in. + ON_CALL(*m_shapeRequests, GetEncompassingAabb).WillByDefault(Return(spawnerBox)); + + // Set up a mock height provider to use the passed-in mock height function to generate a height. + m_terrainAreaHeightRequests = AZStd::make_unique>(entity->GetId()); + ON_CALL(*m_terrainAreaHeightRequests, GetHeight) + .WillByDefault( + [mockHeights](const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists) + { + // By default, set the outPosition to the input position and terrain to always exist. + outPosition = inPosition; + terrainExists = true; + // Let the test function modify these values based on the needs of the specific test. + mockHeights(outPosition, terrainExists); + }); + + ActivateEntity(entity.get()); + return entity; + } + }; - template - AZ::Component* CreateComponent(AZ::Entity* entity) + TEST_F(TerrainSystemTest, TrivialCreateDestroy) { - m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); - return entity->CreateComponent(); + // Trivially verify that the terrain system can successfully be constructed and destructed without errors. + + m_terrainSystem = AZStd::make_unique(); } - // Create a terrain system with reasonable defaults for testing, but with the ability to override the defaults - // on a test-by-test basis. - void CreateAndActivateTerrainSystem( - AZ::Vector2 queryResolution = AZ::Vector2(1.0f), - AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-128.0f), AZ::Vector3(128.0f))) + TEST_F(TerrainSystemTest, TrivialActivateDeactivate) { - // Create the terrain system and give it one tick to fully initialize itself. + // Verify that the terrain system can be activated and deactivated without errors. + m_terrainSystem = AZStd::make_unique(); - m_terrainSystem->SetTerrainAabb(worldBounds); - m_terrainSystem->SetTerrainHeightQueryResolution(queryResolution); m_terrainSystem->Activate(); - AZ::TickBus::Broadcast(&AZ::TickBus::Events::OnTick, 0.f, AZ::ScriptTimePoint{}); + m_terrainSystem->Deactivate(); } - - AZStd::unique_ptr CreateAndActivateMockTerrainLayerSpawner( - const AZ::Aabb& spawnerBox, - const AZStd::function& mockHeights) + TEST_F(TerrainSystemTest, CreateEventsCalledOnActivation) { - // Create the base entity with a mock box shape, Terrain Layer Spawner, and height provider. - auto entity = CreateEntity(); - CreateComponent(entity.get()); - CreateComponent(entity.get()); - - m_boxShapeRequests = AZStd::make_unique>(entity->GetId()); - m_shapeRequests = AZStd::make_unique>(entity->GetId()); - - // Set up the box shape to return whatever spawnerBox was passed in. - ON_CALL(*m_shapeRequests, GetEncompassingAabb).WillByDefault(Return(spawnerBox)); - - // Set up a mock height provider to use the passed-in mock height function to generate a height. - m_terrainAreaHeightRequests = AZStd::make_unique>(entity->GetId()); - ON_CALL(*m_terrainAreaHeightRequests, GetHeight) - .WillByDefault( - [mockHeights](const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists) - { - // By default, set the outPosition to the input position and terrain to always exist. - outPosition = inPosition; - terrainExists = true; - // Let the test function modify these values based on the needs of the specific test. - mockHeights(outPosition, terrainExists); - }); - - ActivateEntity(entity.get()); - return entity; - } -}; + // Verify that when the terrain system is activated, the OnTerrainDataCreate* ebus notifications are generated. -TEST_F(TerrainSystemTest, TrivialCreateDestroy) -{ - // Trivially verify that the terrain system can successfully be constructed and destructed without errors. + NiceMock mockTerrainListener; + EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateBegin()).Times(AtLeast(1)); + EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateEnd()).Times(AtLeast(1)); - m_terrainSystem = AZStd::make_unique(); -} + m_terrainSystem = AZStd::make_unique(); + m_terrainSystem->Activate(); + } -TEST_F(TerrainSystemTest, TrivialActivateDeactivate) -{ - // Verify that the terrain system can be activated and deactivated without errors. + TEST_F(TerrainSystemTest, DestroyEventsCalledOnDeactivation) + { + // Verify that when the terrain system is deactivated, the OnTerrainDataDestroy* ebus notifications are generated. - m_terrainSystem = AZStd::make_unique(); - m_terrainSystem->Activate(); - m_terrainSystem->Deactivate(); -} + NiceMock mockTerrainListener; + EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyBegin()).Times(AtLeast(1)); + EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyEnd()).Times(AtLeast(1)); -TEST_F(TerrainSystemTest, CreateEventsCalledOnActivation) -{ - // Verify that when the terrain system is activated, the OnTerrainDataCreate* ebus notifications are generated. + m_terrainSystem = AZStd::make_unique(); + m_terrainSystem->Activate(); + m_terrainSystem->Deactivate(); + } - NiceMock mockTerrainListener; - EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateBegin()).Times(AtLeast(1)); - EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateEnd()).Times(AtLeast(1)); + TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegistered) + { + // For the terrain system, terrain should only exist where terrain layer spawners are present. - m_terrainSystem = AZStd::make_unique(); - m_terrainSystem->Activate(); -} + // Verify that in the active terrain system, if there are no terrain layer spawners, any arbitrary point + // will return false for terrainExists, returns a height equal to the min world bounds of the terrain system, and returns + // a normal facing up the Z axis. -TEST_F(TerrainSystemTest, DestroyEventsCalledOnDeactivation) -{ - // Verify that when the terrain system is deactivated, the OnTerrainDataDestroy* ebus notifications are generated. + // Create and activate the terrain system with our testing defaults for world bounds and query resolution. + CreateAndActivateTerrainSystem(); - NiceMock mockTerrainListener; - EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyBegin()).Times(AtLeast(1)); - EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyEnd()).Times(AtLeast(1)); + AZ::Aabb worldBounds = m_terrainSystem->GetTerrainAabb(); - m_terrainSystem = AZStd::make_unique(); - m_terrainSystem->Activate(); - m_terrainSystem->Deactivate(); -} + // Loop through several points within the world bounds, including on the edges, and verify that they all return false for + // terrainExists with default heights and normals. + for (float y = worldBounds.GetMin().GetY(); y <= worldBounds.GetMax().GetY(); y += (worldBounds.GetExtents().GetY() / 4.0f)) + { + for (float x = worldBounds.GetMin().GetX(); x <= worldBounds.GetMax().GetX(); x += (worldBounds.GetExtents().GetX() / 4.0f)) + { + AZ::Vector3 position(x, y, 0.0f); + bool terrainExists = true; + float height = + m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); + EXPECT_FALSE(terrainExists); + EXPECT_FLOAT_EQ(height, worldBounds.GetMin().GetZ()); + + terrainExists = true; + AZ::Vector3 normal = + m_terrainSystem->GetNormal(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); + EXPECT_FALSE(terrainExists); + EXPECT_EQ(normal, AZ::Vector3::CreateAxisZ()); + + bool isHole = m_terrainSystem->GetIsHoleFromFloats( + position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); + EXPECT_TRUE(isHole); + } + } + } -TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegistered) -{ - // For the terrain system, terrain should only exist where terrain layer spawners are present. + TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds) + { + // Verify that the presence of a TerrainLayerSpawner causes terrain to exist in (and *only* in) the box where the + // TerrainLayerSpawner is defined. + + // The terrain system should only query Heights from the TerrainAreaHeightRequest bus within the + // TerrainLayerSpawner region, and so those values should only get returned from GetHeight for queries inside that region. + + // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and always returns a height of 5. + constexpr float spawnerHeight = 5.0f; + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(spawnerHeight); + terrainExists = true; + }); - // Verify that in the active terrain system, if there are no terrain layer spawners, any arbitrary point - // will return false for terrainExists, returns a height equal to the min world bounds of the terrain system, and returns - // a normal facing up the Z axis. + // Verify that terrain exists within the layer spawner bounds, and doesn't exist outside of it. - // Create and activate the terrain system with our testing defaults for world bounds and query resolution. - CreateAndActivateTerrainSystem(); + // Create and activate the terrain system with our testing defaults for world bounds and query resolution. + CreateAndActivateTerrainSystem(); - AZ::Aabb worldBounds = m_terrainSystem->GetTerrainAabb(); + // Create a box that's twice as big as the layer spawner box. Loop through it and verify that points within the layer box contain + // terrain and the expected height & normal values, and points outside the layer box don't contain terrain. + const AZ::Aabb encompassingBox = AZ::Aabb::CreateFromMinMax( + spawnerBox.GetMin() - (spawnerBox.GetExtents() / 2.0f), spawnerBox.GetMax() + (spawnerBox.GetExtents() / 2.0f)); - // Loop through several points within the world bounds, including on the edges, and verify that they all return false for - // terrainExists with default heights and normals. - for (float y = worldBounds.GetMin().GetY(); y <= worldBounds.GetMax().GetY(); y += (worldBounds.GetExtents().GetY() / 4.0f)) - { - for (float x = worldBounds.GetMin().GetX(); x <= worldBounds.GetMax().GetX(); x += (worldBounds.GetExtents().GetX() / 4.0f)) + for (float y = encompassingBox.GetMin().GetY(); y < encompassingBox.GetMax().GetY(); y += 1.0f) { - AZ::Vector3 position(x, y, 0.0f); - bool terrainExists = true; - float height = m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); - EXPECT_FALSE(terrainExists); - EXPECT_FLOAT_EQ(height, worldBounds.GetMin().GetZ()); - - terrainExists = true; - AZ::Vector3 normal = m_terrainSystem->GetNormal( - position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); - EXPECT_FALSE(terrainExists); - EXPECT_EQ(normal, AZ::Vector3::CreateAxisZ()); - - bool isHole = m_terrainSystem->GetIsHoleFromFloats( - position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); - EXPECT_TRUE(isHole); + for (float x = encompassingBox.GetMin().GetX(); x < encompassingBox.GetMax().GetX(); x += 1.0f) + { + AZ::Vector3 position(x, y, 0.0f); + bool heightQueryTerrainExists = false; + float height = m_terrainSystem->GetHeight( + position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); + bool isHole = m_terrainSystem->GetIsHoleFromFloats( + position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); + + if (spawnerBox.Contains(AZ::Vector3(position.GetX(), position.GetY(), spawnerBox.GetMin().GetZ()))) + { + EXPECT_TRUE(heightQueryTerrainExists); + EXPECT_FALSE(isHole); + EXPECT_FLOAT_EQ(height, spawnerHeight); + } + else + { + EXPECT_FALSE(heightQueryTerrainExists); + EXPECT_TRUE(isHole); + } + } } } -} -TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds) -{ - // Verify that the presence of a TerrainLayerSpawner causes terrain to exist in (and *only* in) the box where the TerrainLayerSpawner - // is defined. - - // The terrain system should only query Heights from the TerrainAreaHeightRequest bus within the - // TerrainLayerSpawner region, and so those values should only get returned from GetHeight for queries inside that region. - - // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and always returns a height of 5. - constexpr float spawnerHeight = 5.0f; - const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); - auto entity = CreateAndActivateMockTerrainLayerSpawner( - spawnerBox, - [](AZ::Vector3& position, bool& terrainExists) + TEST_F(TerrainSystemTest, TerrainHeightQueriesWithExactSamplersIgnoreQueryGrid) + { + // Verify that when using the "EXACT" height sampler, the returned heights come directly from the height provider at the exact + // requested location, instead of the position being quantized to the height query grid. + + // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and generates a height based on a sine wave + // using a frequency of 1m and an amplitude of 10m. i.e. Heights will range between -10 to 10 meters, but will have a value of 0 + // every 0.5 meters. The sine wave value is based on the absolute X position only, for simplicity. + constexpr float amplitudeMeters = 10.0f; + constexpr float frequencyMeters = 1.0f; + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(amplitudeMeters * sin(AZ::Constants::TwoPi * (position.GetX() / frequencyMeters))); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution that exactly matches + // the frequency of our sine wave. If our height queries rely on the query resolution, we should always get a value of 0. + const AZ::Vector2 queryResolution(frequencyMeters); + CreateAndActivateTerrainSystem(queryResolution); + + // Test an arbitrary set of points that should all produce non-zero heights with the EXACT sampler. They're not aligned with the + // query resolution, or with the 0 points on the sine wave. + const AZ::Vector2 nonZeroPoints[] = { AZ::Vector2(0.3f), AZ::Vector2(2.8f), AZ::Vector2(5.9f), AZ::Vector2(7.7f) }; + for (auto& nonZeroPoint : nonZeroPoints) { - position.SetZ(spawnerHeight); - terrainExists = true; - }); - - // Verify that terrain exists within the layer spawner bounds, and doesn't exist outside of it. - - // Create and activate the terrain system with our testing defaults for world bounds and query resolution. - CreateAndActivateTerrainSystem(); + AZ::Vector3 position(nonZeroPoint.GetX(), nonZeroPoint.GetY(), 0.0f); + bool heightQueryTerrainExists = false; + float height = + m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); - // Create a box that's twice as big as the layer spawner box. Loop through it and verify that points within the layer box contain - // terrain and the expected height & normal values, and points outside the layer box don't contain terrain. - const AZ::Aabb encompassingBox = - AZ::Aabb::CreateFromMinMax(spawnerBox.GetMin() - (spawnerBox.GetExtents() / 2.0f), - spawnerBox.GetMax() + (spawnerBox.GetExtents() / 2.0f)); + // We've chosen a bunch of places on the sine wave that should return a non-zero positive or negative value. + constexpr float epsilon = 0.0001f; + EXPECT_GT(fabsf(height), epsilon); + } - for (float y = encompassingBox.GetMin().GetY(); y < encompassingBox.GetMax().GetY(); y += 1.0f) - { - for (float x = encompassingBox.GetMin().GetX(); x < encompassingBox.GetMax().GetX(); x += 1.0f) + // Test an arbitrary set of points that should all produce zero heights with the EXACT sampler, since they align with 0 points on + // the sine wave, regardless of whether or not they align to the query resolution. + const AZ::Vector2 zeroPoints[] = { AZ::Vector2(0.5f), AZ::Vector2(1.0f), AZ::Vector2(5.0f), AZ::Vector2(7.5f) }; + for (auto& zeroPoint : zeroPoints) { - AZ::Vector3 position(x, y, 0.0f); + AZ::Vector3 position(zeroPoint.GetX(), zeroPoint.GetY(), 0.0f); bool heightQueryTerrainExists = false; float height = m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); - bool isHole = m_terrainSystem->GetIsHoleFromFloats( - position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); - if (spawnerBox.Contains(AZ::Vector3(position.GetX(), position.GetY(), spawnerBox.GetMin().GetZ()))) - { - EXPECT_TRUE(heightQueryTerrainExists); - EXPECT_FALSE(isHole); - EXPECT_FLOAT_EQ(height, spawnerHeight); - } - else - { - EXPECT_FALSE(heightQueryTerrainExists); - EXPECT_TRUE(isHole); - } + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(height, 0.0f, epsilon); } } -} -TEST_F(TerrainSystemTest, TerrainHeightQueriesWithExactSamplersIgnoreQueryGrid) -{ - // Verify that when using the "EXACT" height sampler, the returned heights come directly from the height provider at the exact - // requested location, instead of the position being quantized to the height query grid. - - // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and generates a height based on a sine wave - // using a frequency of 1m and an amplitude of 10m. i.e. Heights will range between -10 to 10 meters, but will have a value of 0 - // every 0.5 meters. The sine wave value is based on the absolute X position only, for simplicity. - constexpr float amplitudeMeters = 10.0f; - constexpr float frequencyMeters = 1.0f; - const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); - auto entity = CreateAndActivateMockTerrainLayerSpawner( - spawnerBox, - [](AZ::Vector3& position, bool& terrainExists) - { - position.SetZ(amplitudeMeters * sin(AZ::Constants::TwoPi * (position.GetX() / frequencyMeters))); - terrainExists = true; - }); - - // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution that exactly matches - // the frequency of our sine wave. If our height queries rely on the query resolution, we should always get a value of 0. - const AZ::Vector2 queryResolution(frequencyMeters); - CreateAndActivateTerrainSystem(queryResolution); - - // Test an arbitrary set of points that should all produce non-zero heights with the EXACT sampler. They're not aligned with the - // query resolution, or with the 0 points on the sine wave. - const AZ::Vector2 nonZeroPoints[] = { AZ::Vector2(0.3f), AZ::Vector2(2.8f), AZ::Vector2(5.9f), AZ::Vector2(7.7f) }; - for (auto& nonZeroPoint : nonZeroPoints) + TEST_F(TerrainSystemTest, TerrainHeightQueriesWithClampSamplersUseQueryGrid) { - AZ::Vector3 position(nonZeroPoint.GetX(), nonZeroPoint.GetY(), 0.0f); + // Verify that when using the "CLAMP" height sampler, the requested location is quantized to the height query grid before fetching + // the height. + + // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal + // to the X + Y position, so if either one doesn't get clamped we'll get an unexpected result. + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(position.GetX() + position.GetY()); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 0.25 meter + // intervals. + const AZ::Vector2 queryResolution(0.25f); + CreateAndActivateTerrainSystem(queryResolution); + + // Test some points and verify that the results always go "downward", whether they're in positive or negative space. + // (Z contains the the expected result for convenience). + const HeightTestPoint testPoints[] = { + { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0.00 + 0.00 + { AZ::Vector2(0.3f, 0.3f), 0.5f }, // Should return a height of 0.25 + 0.25 + { AZ::Vector2(2.8f, 2.8f), 5.5f }, // Should return a height of 2.75 + 2.75 + { AZ::Vector2(5.5f, 5.5f), 11.0f }, // Should return a height of 5.50 + 5.50 + { AZ::Vector2(7.7f, 7.7f), 15.0f }, // Should return a height of 7.50 + 7.50 + + { AZ::Vector2(-0.3f, -0.3f), -1.0f }, // Should return a height of -0.50 + -0.50 + { AZ::Vector2(-2.8f, -2.8f), -6.0f }, // Should return a height of -3.00 + -3.00 + { AZ::Vector2(-5.5f, -5.5f), -11.0f }, // Should return a height of -5.50 + -5.50 + { AZ::Vector2(-7.7f, -7.7f), -15.5f } // Should return a height of -7.75 + -7.75 + }; + for (auto& testPoint : testPoints) + { + const float expectedHeight = testPoint.m_expectedHeight; + + AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); bool heightQueryTerrainExists = false; float height = - m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); + m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP, &heightQueryTerrainExists); - // We've chosen a bunch of places on the sine wave that should return a non-zero positive or negative value. constexpr float epsilon = 0.0001f; - EXPECT_GT(fabsf(height), epsilon); + EXPECT_NEAR(height, expectedHeight, epsilon); + } } - // Test an arbitrary set of points that should all produce zero heights with the EXACT sampler, since they align with 0 points on the - // sine wave, regardless of whether or not they align to the query resolution. - const AZ::Vector2 zeroPoints[] = { AZ::Vector2(0.5f), AZ::Vector2(1.0f), AZ::Vector2(5.0f), AZ::Vector2(7.5f) }; - for (auto& zeroPoint : zeroPoints) + TEST_F(TerrainSystemTest, TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate) { - AZ::Vector3 position(zeroPoint.GetX(), zeroPoint.GetY(), 0.0f); - bool heightQueryTerrainExists = false; - float height = - m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); + // Verify that when using the "BILINEAR" height sampler, the heights are interpolated from points sampled from the query grid. + + // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal + // to the X + Y position, so we'll have heights that look like this on our grid: + // 0 *---* 1 + // | | + // 1 *---* 2 + // However, everywhere inside the grid box, we'll generate heights much larger than X + Y. It will have no effect on exact grid + // points, but it will noticeably affect the expected height values if any points get sampled in-between grid points. + + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); + const float amplitudeMeters = 10.0f; + const float frequencyMeters = 1.0f; + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists) + { + // Our generated height will be X + Y. + float expectedHeight = position.GetX() + position.GetY(); + + // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height. + // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries. + float unexpectedVariance = + amplitudeMeters * (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters)); + position.SetZ(expectedHeight + unexpectedVariance); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals. + const AZ::Vector2 queryResolution(frequencyMeters); + CreateAndActivateTerrainSystem(queryResolution); + + // Test some points and verify that the results are the expected bilinear filtered result, + // whether they're in positive or negative space. + // (Z contains the the expected result for convenience). + const HeightTestPoint testPoints[] = { + + // Queries directly on grid points. These should return values of X + Y. + { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0 + 0 + { AZ::Vector2(1.0f, 0.0f), 1.0f }, // Should return a height of 1 + 0 + { AZ::Vector2(0.0f, 1.0f), 1.0f }, // Should return a height of 0 + 1 + { AZ::Vector2(1.0f, 1.0f), 2.0f }, // Should return a height of 1 + 1 + { AZ::Vector2(3.0f, 5.0f), 8.0f }, // Should return a height of 3 + 5 + + { AZ::Vector2(-1.0f, 0.0f), -1.0f }, // Should return a height of -1 + 0 + { AZ::Vector2(0.0f, -1.0f), -1.0f }, // Should return a height of 0 + -1 + { AZ::Vector2(-1.0f, -1.0f), -2.0f }, // Should return a height of -1 + -1 + { AZ::Vector2(-3.0f, -5.0f), -8.0f }, // Should return a height of -3 + -5 + + // Queries that are on a grid edge (one axis on the grid, the other somewhere in-between). + // These should just be a linear interpolation of the points, so it should still be X + Y. + + { AZ::Vector2(0.25f, 0.0f), 0.25f }, // Should return a height of -0.25 + 0 + { AZ::Vector2(3.75f, 0.0f), 3.75f }, // Should return a height of -3.75 + 0 + { AZ::Vector2(0.0f, 0.25f), 0.25f }, // Should return a height of 0 + -0.25 + { AZ::Vector2(0.0f, 3.75f), 3.75f }, // Should return a height of 0 + -3.75 + + { AZ::Vector2(2.0f, 3.75f), 5.75f }, // Should return a height of -2 + -3.75 + { AZ::Vector2(2.25f, 4.0f), 6.25f }, // Should return a height of -2.25 + -4 + + { AZ::Vector2(-0.25f, 0.0f), -0.25f }, // Should return a height of -0.25 + 0 + { AZ::Vector2(-3.75f, 0.0f), -3.75f }, // Should return a height of -3.75 + 0 + { AZ::Vector2(0.0f, -0.25f), -0.25f }, // Should return a height of 0 + -0.25 + { AZ::Vector2(0.0f, -3.75f), -3.75f }, // Should return a height of 0 + -3.75 + + { AZ::Vector2(-2.0f, -3.75f), -5.75f }, // Should return a height of -2 + -3.75 + { AZ::Vector2(-2.25f, -4.0f), -6.25f }, // Should return a height of -2.25 + -4 + + // Queries inside a grid square (both axes are in-between grid points) + // This is a full bilinear interpolation, but because we're using X + Y for our heights, the interpolated values + // should *still* be X + Y assuming the points were sampled correctly from the grid points. + + { AZ::Vector2(3.25f, 5.25f), 8.5f }, // Should return a height of 3.25 + 5.25 + { AZ::Vector2(7.71f, 9.74f), 17.45f }, // Should return a height of 7.71 + 9.74 + + { AZ::Vector2(-3.25f, -5.25f), -8.5f }, // Should return a height of -3.25 + -5.25 + { AZ::Vector2(-7.71f, -9.74f), -17.45f }, // Should return a height of -7.71 + -9.74 + }; + + // Loop through every test point and validate it. + for (auto& testPoint : testPoints) + { + const float expectedHeight = testPoint.m_expectedHeight; + + AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); + bool heightQueryTerrainExists = false; + float height = m_terrainSystem->GetHeight( + position, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &heightQueryTerrainExists); - constexpr float epsilon = 0.0001f; - EXPECT_NEAR(height, 0.0f, epsilon); + // Verify that our height query returned the bilinear filtered result we expect. + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(height, expectedHeight, epsilon); + } } -} -TEST_F(TerrainSystemTest, TerrainHeightQueriesWithClampSamplersUseQueryGrid) -{ - // Verify that when using the "CLAMP" height sampler, the requested location is quantized to the height query grid before fetching - // the height. - - // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal - // to the X + Y position, so if either one doesn't get clamped we'll get an unexpected result. - const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); - auto entity = CreateAndActivateMockTerrainLayerSpawner( - spawnerBox, - [](AZ::Vector3& position, bool& terrainExists) + TEST_F(TerrainSystemTest, GetSurfaceWeightsReturnsAllValidSurfaceWeightsInOrder) + { + // When there is more than one surface/weight defined, they should all be returned in descending weight order. + + CreateAndActivateTerrainSystem(); + + const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3::CreateZero(), AZ::Vector3::CreateOne()); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + aabb, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(1.0f); + terrainExists = true; + }); + + const AZ::Crc32 tag1("tag1"); + const AZ::Crc32 tag2("tag2"); + const AZ::Crc32 tag3("tag3"); + const float tag1Weight = 0.8f; + const float tag2Weight = 1.0f; + const float tag3Weight = 0.5f; + + AzFramework::SurfaceData::SurfaceTagWeightList orderedSurfaceWeights { - position.SetZ(position.GetX() + position.GetY()); - terrainExists = true; - }); + { tag1, tag1Weight }, { tag2, tag2Weight }, { tag3, tag3Weight } + }; - // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 0.25 meter intervals. - const AZ::Vector2 queryResolution(0.25f); - CreateAndActivateTerrainSystem(queryResolution); + NiceMock mockSurfaceRequests(entity->GetId()); + ON_CALL(mockSurfaceRequests, GetSurfaceWeights).WillByDefault(SetArgReferee<1>(orderedSurfaceWeights)); - // Test some points and verify that the results always go "downward", whether they're in positive or negative space. - // (Z contains the the expected result for convenience). - const HeightTestPoint testPoints[] = - { - { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0.00 + 0.00 - { AZ::Vector2(0.3f, 0.3f), 0.5f }, // Should return a height of 0.25 + 0.25 - { AZ::Vector2(2.8f, 2.8f), 5.5f }, // Should return a height of 2.75 + 2.75 - { AZ::Vector2(5.5f, 5.5f), 11.0f }, // Should return a height of 5.50 + 5.50 - { AZ::Vector2(7.7f, 7.7f), 15.0f }, // Should return a height of 7.50 + 7.50 - - { AZ::Vector2(-0.3f, -0.3f), -1.0f }, // Should return a height of -0.50 + -0.50 - { AZ::Vector2(-2.8f, -2.8f), -6.0f }, // Should return a height of -3.00 + -3.00 - { AZ::Vector2(-5.5f, -5.5f), -11.0f }, // Should return a height of -5.50 + -5.50 - { AZ::Vector2(-7.7f, -7.7f), -15.5f } // Should return a height of -7.75 + -7.75 - }; - for (auto& testPoint : testPoints) - { - const float expectedHeight = testPoint.m_expectedHeight; + AzFramework::SurfaceData::SurfaceTagWeightList outSurfaceWeights; - AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); - bool heightQueryTerrainExists = false; - float height = - m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP, &heightQueryTerrainExists); + // Asking for values outside the layer spawner bounds, should result in no results. + m_terrainSystem->GetSurfaceWeights(aabb.GetMax() + AZ::Vector3::CreateOne(), outSurfaceWeights); + EXPECT_TRUE(outSurfaceWeights.empty()); - constexpr float epsilon = 0.0001f; - EXPECT_NEAR(height, expectedHeight, epsilon); - } -} + // Inside the layer spawner box should give us all of the added surface weights. + m_terrainSystem->GetSurfaceWeights(aabb.GetCenter(), outSurfaceWeights); -TEST_F(TerrainSystemTest, TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate) -{ - // Verify that when using the "BILINEAR" height sampler, the heights are interpolated from points sampled from the query grid. - - // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal - // to the X + Y position, so we'll have heights that look like this on our grid: - // 0 *---* 1 - // | | - // 1 *---* 2 - // However, everywhere inside the grid box, we'll generate heights much larger than X + Y. It will have no effect on exact grid - // points, but it will noticeably affect the expected height values if any points get sampled in-between grid points. - - const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); - const float amplitudeMeters = 10.0f; - const float frequencyMeters = 1.0f; - auto entity = CreateAndActivateMockTerrainLayerSpawner( - spawnerBox, - [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists) + EXPECT_EQ(outSurfaceWeights.size(), 3); + + // The weights should be returned in decreasing order. + AZ::Crc32 expectedCrcList[] = { tag2, tag1, tag3 }; + const float expectedWeightList[] = { tag2Weight, tag1Weight, tag3Weight }; + + int index = 0; + for (const auto& surfaceWeight : outSurfaceWeights) { - // Our generated height will be X + Y. - float expectedHeight = position.GetX() + position.GetY(); - - // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height. - // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries. - float unexpectedVariance = amplitudeMeters * - (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters)); - position.SetZ(expectedHeight + unexpectedVariance); - terrainExists = true; - }); - - // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals. - const AZ::Vector2 queryResolution(frequencyMeters); - CreateAndActivateTerrainSystem(queryResolution); - - // Test some points and verify that the results are the expected bilinear filtered result, - // whether they're in positive or negative space. - // (Z contains the the expected result for convenience). - const HeightTestPoint testPoints[] = { - - // Queries directly on grid points. These should return values of X + Y. - { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0 + 0 - { AZ::Vector2(1.0f, 0.0f), 1.0f }, // Should return a height of 1 + 0 - { AZ::Vector2(0.0f, 1.0f), 1.0f }, // Should return a height of 0 + 1 - { AZ::Vector2(1.0f, 1.0f), 2.0f }, // Should return a height of 1 + 1 - { AZ::Vector2(3.0f, 5.0f), 8.0f }, // Should return a height of 3 + 5 - - { AZ::Vector2(-1.0f, 0.0f), -1.0f }, // Should return a height of -1 + 0 - { AZ::Vector2(0.0f, -1.0f), -1.0f }, // Should return a height of 0 + -1 - { AZ::Vector2(-1.0f, -1.0f), -2.0f }, // Should return a height of -1 + -1 - { AZ::Vector2(-3.0f, -5.0f), -8.0f }, // Should return a height of -3 + -5 - - // Queries that are on a grid edge (one axis on the grid, the other somewhere in-between). - // These should just be a linear interpolation of the points, so it should still be X + Y. - - { AZ::Vector2(0.25f, 0.0f), 0.25f }, // Should return a height of -0.25 + 0 - { AZ::Vector2(3.75f, 0.0f), 3.75f }, // Should return a height of -3.75 + 0 - { AZ::Vector2(0.0f, 0.25f), 0.25f }, // Should return a height of 0 + -0.25 - { AZ::Vector2(0.0f, 3.75f), 3.75f }, // Should return a height of 0 + -3.75 - - { AZ::Vector2(2.0f, 3.75f), 5.75f }, // Should return a height of -2 + -3.75 - { AZ::Vector2(2.25f, 4.0f), 6.25f }, // Should return a height of -2.25 + -4 - - { AZ::Vector2(-0.25f, 0.0f), -0.25f }, // Should return a height of -0.25 + 0 - { AZ::Vector2(-3.75f, 0.0f), -3.75f }, // Should return a height of -3.75 + 0 - { AZ::Vector2(0.0f, -0.25f), -0.25f }, // Should return a height of 0 + -0.25 - { AZ::Vector2(0.0f, -3.75f), -3.75f }, // Should return a height of 0 + -3.75 - - { AZ::Vector2(-2.0f, -3.75f), -5.75f }, // Should return a height of -2 + -3.75 - { AZ::Vector2(-2.25f, -4.0f), -6.25f }, // Should return a height of -2.25 + -4 - - // Queries inside a grid square (both axes are in-between grid points) - // This is a full bilinear interpolation, but because we're using X + Y for our heights, the interpolated values - // should *still* be X + Y assuming the points were sampled correctly from the grid points. - - { AZ::Vector2(3.25f, 5.25f), 8.5f }, // Should return a height of 3.25 + 5.25 - { AZ::Vector2(7.71f, 9.74f), 17.45f }, // Should return a height of 7.71 + 9.74 - - { AZ::Vector2(-3.25f, -5.25f), -8.5f }, // Should return a height of -3.25 + -5.25 - { AZ::Vector2(-7.71f, -9.74f), -17.45f }, // Should return a height of -7.71 + -9.74 - }; + EXPECT_EQ(surfaceWeight.m_surfaceType, expectedCrcList[index]); + EXPECT_NEAR(surfaceWeight.m_weight, expectedWeightList[index], 0.01f); + index++; + } + } - // Loop through every test point and validate it. - for (auto& testPoint : testPoints) + TEST_F(TerrainSystemTest, GetMaxSurfaceWeightsReturnsBiggestValidSurfaceWeight) { - const float expectedHeight = testPoint.m_expectedHeight; + CreateAndActivateTerrainSystem(); + + const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3::CreateZero(), AZ::Vector3::CreateOne()); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + aabb, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(1.0f); + terrainExists = true; + }); + + const AZ::Crc32 tag1("tag1"); + const AZ::Crc32 tag2("tag2"); + + AzFramework::SurfaceData::SurfaceTagWeightList orderedSurfaceWeights; + + AzFramework::SurfaceData::SurfaceTagWeight tagWeight1; + tagWeight1.m_surfaceType = tag1; + tagWeight1.m_weight = 1.0f; + orderedSurfaceWeights.emplace_back(tagWeight1); + + AzFramework::SurfaceData::SurfaceTagWeight tagWeight2; + tagWeight2.m_surfaceType = tag2; + tagWeight2.m_weight = 0.8f; + orderedSurfaceWeights.emplace_back(tagWeight2); + + NiceMock mockSurfaceRequests(entity->GetId()); + ON_CALL(mockSurfaceRequests, GetSurfaceWeights).WillByDefault(SetArgReferee<1>(orderedSurfaceWeights)); + + // Asking for values outside the layer spawner bounds, should result in an invalid result. + AzFramework::SurfaceData::SurfaceTagWeight tagWeight = + m_terrainSystem->GetMaxSurfaceWeight(aabb.GetMax() + AZ::Vector3::CreateOne()); + + EXPECT_EQ(tagWeight.m_surfaceType, AZ::Crc32(AzFramework::SurfaceData::Constants::s_unassignedTagName)); - AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); - bool heightQueryTerrainExists = false; - float height = - m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &heightQueryTerrainExists); + // Inside the layer spawner box should give us the highest weighted tag (tag1). + tagWeight = m_terrainSystem->GetMaxSurfaceWeight(aabb.GetCenter()); - // Verify that our height query returned the bilinear filtered result we expect. - constexpr float epsilon = 0.0001f; - EXPECT_NEAR(height, expectedHeight, epsilon); + EXPECT_EQ(tagWeight.m_surfaceType, tagWeight1.m_surfaceType); + EXPECT_NEAR(tagWeight.m_weight, tagWeight1.m_weight, 0.01f); } -} +} // namespace UnitTest diff --git a/Gems/Terrain/Code/terrain_editor_shared_files.cmake b/Gems/Terrain/Code/terrain_editor_shared_files.cmake index 334c89ec73..09724751a9 100644 --- a/Gems/Terrain/Code/terrain_editor_shared_files.cmake +++ b/Gems/Terrain/Code/terrain_editor_shared_files.cmake @@ -11,6 +11,8 @@ set(FILES Source/EditorComponents/EditorTerrainHeightGradientListComponent.h Source/EditorComponents/EditorTerrainLayerSpawnerComponent.cpp Source/EditorComponents/EditorTerrainLayerSpawnerComponent.h + Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp + Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h Source/EditorComponents/EditorTerrainSurfaceGradientListComponent.cpp Source/EditorComponents/EditorTerrainSurfaceGradientListComponent.h Source/EditorComponents/EditorTerrainWorldComponent.cpp diff --git a/Gems/Terrain/Code/terrain_files.cmake b/Gems/Terrain/Code/terrain_files.cmake index 780dd1bdb0..893477e10d 100644 --- a/Gems/Terrain/Code/terrain_files.cmake +++ b/Gems/Terrain/Code/terrain_files.cmake @@ -12,6 +12,8 @@ set(FILES Source/Components/TerrainHeightGradientListComponent.h Source/Components/TerrainLayerSpawnerComponent.cpp Source/Components/TerrainLayerSpawnerComponent.h + Source/Components/TerrainPhysicsColliderComponent.cpp + Source/Components/TerrainPhysicsColliderComponent.h Source/Components/TerrainSurfaceDataSystemComponent.cpp Source/Components/TerrainSurfaceDataSystemComponent.h Source/Components/TerrainSurfaceGradientListComponent.cpp diff --git a/Gems/Terrain/Code/terrain_mocks_files.cmake b/Gems/Terrain/Code/terrain_mocks_files.cmake index 2aedd1c5d8..874e17f028 100644 --- a/Gems/Terrain/Code/terrain_mocks_files.cmake +++ b/Gems/Terrain/Code/terrain_mocks_files.cmake @@ -8,4 +8,6 @@ set(FILES Mocks/Terrain/MockTerrain.h + Mocks/Terrain/MockTerrainLayerSpawner.h + Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h ) diff --git a/Gems/Terrain/Code/terrain_tests_files.cmake b/Gems/Terrain/Code/terrain_tests_files.cmake index 793bc01ac5..3e37e509a7 100644 --- a/Gems/Terrain/Code/terrain_tests_files.cmake +++ b/Gems/Terrain/Code/terrain_tests_files.cmake @@ -10,6 +10,9 @@ set(FILES Tests/TerrainTest.cpp Tests/TerrainSystemTest.cpp Tests/LayerSpawnerTests.cpp + Tests/TerrainPhysicsColliderTests.cpp Tests/SurfaceMaterialsListTest.cpp Tests/MockAxisAlignedBoxShapeComponent.h + Tests/TerrainHeightGradientListTests.cpp + Tests/TerrainSurfaceGradientListTests.cpp ) diff --git a/Tools/LyTestTools/ly_test_tools/launchers/launcher_helper.py b/Tools/LyTestTools/ly_test_tools/launchers/launcher_helper.py index d37623f352..ac4ad621da 100755 --- a/Tools/LyTestTools/ly_test_tools/launchers/launcher_helper.py +++ b/Tools/LyTestTools/ly_test_tools/launchers/launcher_helper.py @@ -9,14 +9,15 @@ Main launchers module, provides a facade for creating launchers. import logging -import ly_test_tools._internal.managers.workspace import ly_test_tools +import ly_test_tools._internal.managers.workspace as workspace_manager +import ly_test_tools.launchers.platforms.base as base_launcher log = logging.getLogger(__name__) def create_launcher(workspace, launcher_platform=ly_test_tools.HOST_OS_PLATFORM, args=None): - # type: (ly_test_tools.managers.workspace.WorkspaceManager, str, List[str]) -> Launcher + # type: (workspace_manager.AbstractWorkspaceManager, str, list[str]) -> base_launcher.Launcher """ Create a launcher compatible with the specified workspace, if no specific launcher is found return a generic one. @@ -25,12 +26,16 @@ def create_launcher(workspace, launcher_platform=ly_test_tools.HOST_OS_PLATFORM, :param args: List of arguments to pass to the launcher's 'args' argument during construction :return: Launcher instance """ - launcher_class = ly_test_tools.LAUNCHERS.get(launcher_platform, ly_test_tools.HOST_OS_PLATFORM) + launcher_class = ly_test_tools.LAUNCHERS.get(launcher_platform) + if not launcher_class: + log.warning(f"Using default launcher for '{ly_test_tools.HOST_OS_PLATFORM}' " + f"as no option is available for '{launcher_platform}'") + launcher_class = ly_test_tools.LAUNCHERS.get(ly_test_tools.HOST_OS_PLATFORM) return launcher_class(workspace, args) def create_dedicated_launcher(workspace, launcher_platform=ly_test_tools.HOST_OS_DEDICATED_SERVER, args=None): - # type: (ly_test_tools.managers.workspace.WorkspaceManager, str, List[str]) -> Launcher + # type: (workspace_manager.AbstractWorkspaceManager, str, list[str]) -> base_launcher.Launcher """ Create a dedicated launcher compatible with the specified workspace. Dedicated Launcher is only supported on the Linux and Windows Platform @@ -40,12 +45,16 @@ def create_dedicated_launcher(workspace, launcher_platform=ly_test_tools.HOST_OS :param args: List of arguments to pass to the launcher's 'args' argument during construction :return: Launcher instance """ - launcher_class = ly_test_tools.LAUNCHERS.get(launcher_platform, ly_test_tools.HOST_OS_DEDICATED_SERVER) + launcher_class = ly_test_tools.LAUNCHERS.get(launcher_platform) + if not launcher_class: + log.warning(f"Using default dedicated launcher for '{ly_test_tools.HOST_OS_DEDICATED_SERVER}' " + f"as no option is available for '{launcher_platform}'") + launcher_class = ly_test_tools.LAUNCHERS.get(ly_test_tools.HOST_OS_DEDICATED_SERVER) return launcher_class(workspace, args) def create_editor(workspace, launcher_platform=ly_test_tools.HOST_OS_EDITOR, args=None): - # type: (ly_test_tools.managers.workspace.WorkspaceManager, str, List[str]) -> Launcher + # type: (workspace_manager.AbstractWorkspaceManager, str, list[str]) -> base_launcher.Launcher """ Create an Editor compatible with the specified workspace. Editor is only officially supported on the Windows Platform. @@ -55,12 +64,16 @@ def create_editor(workspace, launcher_platform=ly_test_tools.HOST_OS_EDITOR, arg :param args: List of arguments to pass to the launcher's 'args' argument during construction :return: Editor instance """ - launcher_class = ly_test_tools.LAUNCHERS.get(launcher_platform, ly_test_tools.HOST_OS_EDITOR) + launcher_class = ly_test_tools.LAUNCHERS.get(launcher_platform) + if not launcher_class: + log.warning(f"Using default editor launcher for '{ly_test_tools.HOST_OS_EDITOR}' " + f"as no option is available for '{launcher_platform}'") + launcher_class = ly_test_tools.LAUNCHERS.get(ly_test_tools.HOST_OS_EDITOR) return launcher_class(workspace, args) def create_generic_launcher(workspace, launcher_platform, exe_file_name, args=None): - # type: (ly_test_tools.managers.workspace.WorkspaceManager, str, str, List[str]) -> Launcher + # type: (workspace_manager.AbstractWorkspaceManager, str, str, list[str]) -> base_launcher.Launcher """ Create a generic launcher compatible with the specified workspace. Allows custom .exe files to serve as the launcher instead of ones listed in the ly_test_tools.LAUNCHERS constant @@ -71,5 +84,9 @@ def create_generic_launcher(workspace, launcher_platform, exe_file_name, args=No :param args: List of arguments to pass to the launcher's 'args' argument during construction :return: Launcher instance. """ - launcher_class = ly_test_tools.LAUNCHERS.get(launcher_platform, ly_test_tools.HOST_OS_GENERIC_EXECUTABLE) + launcher_class = ly_test_tools.LAUNCHERS.get(launcher_platform) + if not launcher_class: + log.warning(f"Using default generic executable launcher for '{ly_test_tools.HOST_OS_GENERIC_EXECUTABLE}' " + f"as no option is available for '{launcher_platform}'") + launcher_class = ly_test_tools.LAUNCHERS.get(ly_test_tools.HOST_OS_GENERIC_EXECUTABLE) return launcher_class(workspace, exe_file_name, args) diff --git a/Tools/LyTestTools/setup.py b/Tools/LyTestTools/setup.py index e23f914dab..a523cbc2dd 100755 --- a/Tools/LyTestTools/setup.py +++ b/Tools/LyTestTools/setup.py @@ -48,7 +48,8 @@ if __name__ == '__main__': 'ly_test_tools=ly_test_tools._internal.pytest_plugin.test_tools_fixtures', 'testrail_filter=ly_test_tools._internal.pytest_plugin.case_id', 'terminal_report=ly_test_tools._internal.pytest_plugin.terminal_report', - 'editor_test=ly_test_tools._internal.pytest_plugin.editor_test' + 'editor_test=ly_test_tools._internal.pytest_plugin.editor_test', + 'pytester=_pytest.pytester' ], }, ) diff --git a/Tools/LyTestTools/tests/unit/test_launcher_base.py b/Tools/LyTestTools/tests/unit/test_launcher_base.py index 1ab9129a7d..5264e1bc28 100755 --- a/Tools/LyTestTools/tests/unit/test_launcher_base.py +++ b/Tools/LyTestTools/tests/unit/test_launcher_base.py @@ -221,3 +221,10 @@ class TestLauncherBuilder(object): under_test = ly_test_tools.launchers.launcher_helper.create_editor( dummy_workspace, ly_test_tools.HOST_OS_GENERIC_EXECUTABLE) assert isinstance(under_test, ly_test_tools.launchers.Launcher) + + def test_CreateEditor_InvalidPlatform_ValidLauncherStillReturned(self): + dummy_workspace = mock.MagicMock() + dummy_workspace.paths.build_directory.return_value = 'dummy' + under_test = ly_test_tools.launchers.launcher_helper.create_editor( + dummy_workspace, 'does not exist') + assert isinstance(under_test, ly_test_tools.launchers.Launcher) diff --git a/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake b/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake index d6fdc5cd9b..6210b9cc18 100644 --- a/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake +++ b/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake @@ -39,7 +39,7 @@ ly_associate_package(PACKAGE_NAME libsamplerate-0.2.1-rev2-linux ly_associate_package(PACKAGE_NAME OpenSSL-1.1.1b-rev2-linux TARGETS OpenSSL PACKAGE_HASH b779426d1e9c5ddf71160d5ae2e639c3b956e0fb5e9fcaf9ce97c4526024e3bc) ly_associate_package(PACKAGE_NAME DirectXShaderCompilerDxc-1.6.2104-o3de-rev3-linux TARGETS DirectXShaderCompilerDxc PACKAGE_HASH 88c4a359325d749bc34090b9ac466424847f3b71ba0de15045cf355c17c07099) ly_associate_package(PACKAGE_NAME SPIRVCross-2021.04.29-rev1-linux TARGETS SPIRVCross PACKAGE_HASH 7889ee5460a688e9b910c0168b31445c0079d363affa07b25d4c8aeb608a0b80) -ly_associate_package(PACKAGE_NAME azslc-1.7.23-rev2-linux TARGETS azslc PACKAGE_HASH 1ba84d8321a566d35a1e9aa7400211ba8e6d1c11c08e4be3c93e6e74b8f7aef1) +ly_associate_package(PACKAGE_NAME azslc-1.7.34-rev1-linux TARGETS azslc PACKAGE_HASH 6d7dc671936c34ff70d2632196107ca1b8b2b41acdd021bfbc69a9fd56215c22) ly_associate_package(PACKAGE_NAME zlib-1.2.11-rev5-linux TARGETS ZLIB PACKAGE_HASH 9be5ea85722fc27a8645a9c8a812669d107c68e6baa2ca0740872eaeb6a8b0fc) ly_associate_package(PACKAGE_NAME squish-ccr-deb557d-rev1-linux TARGETS squish-ccr PACKAGE_HASH 85fecafbddc6a41a27c5f59ed4a5dfb123a94cb4666782cf26e63c0a4724c530) ly_associate_package(PACKAGE_NAME astc-encoder-3.2-rev1-linux TARGETS astc-encoder PACKAGE_HASH 2ba97a06474d609945f0ab4419af1f6bbffdd294ca6b869f5fcebec75c573c0f) diff --git a/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake b/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake index 41df718b71..d054ba22e1 100644 --- a/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake +++ b/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake @@ -17,7 +17,6 @@ ly_associate_package(PACKAGE_NAME cityhash-1.1-multiplatform ly_associate_package(PACKAGE_NAME expat-2.1.0-multiplatform TARGETS expat PACKAGE_HASH 452256acd1fd699cef24162575b3524fccfb712f5321c83f1df1ce878de5b418) ly_associate_package(PACKAGE_NAME zstd-1.35-multiplatform TARGETS zstd PACKAGE_HASH 45d466c435f1095898578eedde85acf1fd27190e7ea99aeaa9acfd2f09e12665) ly_associate_package(PACKAGE_NAME SQLite-3.32.2-rev3-multiplatform TARGETS SQLite PACKAGE_HASH dd4d3de6cbb4ce3d15fc504ba0ae0587e515dc89a25228037035fc0aef4831f4) -ly_associate_package(PACKAGE_NAME azslc-1.7.23-rev1-multiplatform TARGETS azslc PACKAGE_HASH 664439954bad54cc43731c684adbc1249d971ad7379fcd83ca8bba5e1cc4a2d0) ly_associate_package(PACKAGE_NAME glad-2.0.0-beta-rev2-multiplatform TARGETS glad PACKAGE_HASH ff97ee9664e97d0854b52a3734c2289329d9f2b4cd69478df6d0ca1f1c9392ee) ly_associate_package(PACKAGE_NAME lux_core-2.2-rev5-multiplatform TARGETS lux_core PACKAGE_HASH c8c13cf7bc351643e1abd294d0841b24dee60e51647dff13db7aec396ad1e0b5) ly_associate_package(PACKAGE_NAME xxhash-0.7.4-rev1-multiplatform TARGETS xxhash PACKAGE_HASH e81f3e6c4065975833996dd1fcffe46c3cf0f9e3a4207ec5f4a1b564ba75861e) @@ -43,4 +42,4 @@ ly_associate_package(PACKAGE_NAME squish-ccr-deb557d-rev1-mac ly_associate_package(PACKAGE_NAME astc-encoder-3.2-rev1-mac TARGETS astc-encoder PACKAGE_HASH 96f6ea8c3e45ec7fe525230c7c53ca665c8300d8e28456cc19bb3159ce6f8dcc) ly_associate_package(PACKAGE_NAME ISPCTexComp-36b80aa-rev1-mac TARGETS ISPCTexComp PACKAGE_HASH 8a4e93277b8face6ea2fd57c6d017bdb55643ed3d6387110bc5f6b3b884dd169) ly_associate_package(PACKAGE_NAME lz4-1.9.3-vcpkg-rev4-mac TARGETS lz4 PACKAGE_HASH 891ff630bf34f7ab1d8eaee2ea0a8f1fca89dbdc63fca41ee592703dd488a73b) - +ly_associate_package(PACKAGE_NAME azslc-1.7.34-rev1-mac TARGETS azslc PACKAGE_HASH a9d81946b42ffa55c0d14d6a9249b3340e59a8fb8835e7a96c31df80f14723bc) diff --git a/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake b/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake index cccd2591e8..fce2229771 100644 --- a/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake +++ b/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake @@ -17,7 +17,6 @@ ly_associate_package(PACKAGE_NAME cityhash-1.1-multiplatform ly_associate_package(PACKAGE_NAME expat-2.1.0-multiplatform TARGETS expat PACKAGE_HASH 452256acd1fd699cef24162575b3524fccfb712f5321c83f1df1ce878de5b418) ly_associate_package(PACKAGE_NAME zstd-1.35-multiplatform TARGETS zstd PACKAGE_HASH 45d466c435f1095898578eedde85acf1fd27190e7ea99aeaa9acfd2f09e12665) ly_associate_package(PACKAGE_NAME SQLite-3.32.2-rev3-multiplatform TARGETS SQLite PACKAGE_HASH dd4d3de6cbb4ce3d15fc504ba0ae0587e515dc89a25228037035fc0aef4831f4) -ly_associate_package(PACKAGE_NAME azslc-1.7.23-rev1-multiplatform TARGETS azslc PACKAGE_HASH 664439954bad54cc43731c684adbc1249d971ad7379fcd83ca8bba5e1cc4a2d0) ly_associate_package(PACKAGE_NAME glad-2.0.0-beta-rev2-multiplatform TARGETS glad PACKAGE_HASH ff97ee9664e97d0854b52a3734c2289329d9f2b4cd69478df6d0ca1f1c9392ee) ly_associate_package(PACKAGE_NAME lux_core-2.2-rev5-multiplatform TARGETS lux_core PACKAGE_HASH c8c13cf7bc351643e1abd294d0841b24dee60e51647dff13db7aec396ad1e0b5) ly_associate_package(PACKAGE_NAME xxhash-0.7.4-rev1-multiplatform TARGETS xxhash PACKAGE_HASH e81f3e6c4065975833996dd1fcffe46c3cf0f9e3a4207ec5f4a1b564ba75861e) @@ -50,3 +49,4 @@ ly_associate_package(PACKAGE_NAME squish-ccr-deb557d-rev1-windows ly_associate_package(PACKAGE_NAME astc-encoder-3.2-rev1-windows TARGETS astc-encoder PACKAGE_HASH 3addc6fc1a7eb0d6b7f3d530e962af967e6d92b3825ef485da243346357cf78e) ly_associate_package(PACKAGE_NAME ISPCTexComp-36b80aa-rev1-windows TARGETS ISPCTexComp PACKAGE_HASH b6fa6ea28a2808a9a5524c72c37789c525925e435770f2d94eb2d387360fa2d0) ly_associate_package(PACKAGE_NAME lz4-1.9.3-vcpkg-rev4-windows TARGETS lz4 PACKAGE_HASH 4ea457b833cd8cfaf8e8e06ed6df601d3e6783b606bdbc44a677f77e19e0db16) +ly_associate_package(PACKAGE_NAME azslc-1.7.34-rev1-windows TARGETS azslc PACKAGE_HASH 44eb2e0fc4b0f1c75d0fb6f24c93a5753655b84dbc3e6ad45389ed3b9cf7a4b0) diff --git a/cmake/LYTestWrappers.cmake b/cmake/LYTestWrappers.cmake index 87649c5be6..9b139645fb 100644 --- a/cmake/LYTestWrappers.cmake +++ b/cmake/LYTestWrappers.cmake @@ -17,7 +17,7 @@ set(LY_GOOGLETEST_EXTRA_PARAMS CACHE STRING "Allows injection of additional opti find_package(Python REQUIRED MODULE) -ly_set(LY_PYTEST_EXECUTABLE ${LY_PYTHON_CMD} -B -m pytest -v --tb=short --show-capture=log -c ${LY_ROOT_FOLDER}/ctest_pytest.ini --build-directory "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$") +ly_set(LY_PYTEST_EXECUTABLE ${LY_PYTHON_CMD} -B -m pytest -v --tb=short --show-capture=log -c ${LY_ROOT_FOLDER}/pytest.ini --build-directory "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$") ly_set(LY_TEST_GLOBAL_KNOWN_SUITE_NAMES "smoke" "main" "periodic" "benchmark" "sandbox" "awsi") ly_set(LY_TEST_GLOBAL_KNOWN_REQUIREMENTS "gpu") diff --git a/cmake/Platform/Common/Install_common.cmake b/cmake/Platform/Common/Install_common.cmake index c453fadd2e..44cdeb1994 100644 --- a/cmake/Platform/Common/Install_common.cmake +++ b/cmake/Platform/Common/Install_common.cmake @@ -364,7 +364,7 @@ function(ly_setup_o3de_install) # Misc install(FILES - ${LY_ROOT_FOLDER}/ctest_pytest.ini + ${LY_ROOT_FOLDER}/pytest.ini ${LY_ROOT_FOLDER}/LICENSE.txt ${LY_ROOT_FOLDER}/README.md DESTINATION . diff --git a/ctest_pytest.ini b/pytest.ini similarity index 91% rename from ctest_pytest.ini rename to pytest.ini index 93310a522d..65c93e0eb2 100644 --- a/ctest_pytest.ini +++ b/pytest.ini @@ -7,10 +7,11 @@ # [pytest] -python_files = 'test_*.py' , '*_test.py' , '*_tests.py' +python_files = 'test_*.py' , '*_test.py' , '*_tests.py', 'TestSuite_*.py' norecursedirs = Python/2.7.* Python/3.7.5 BinTemp Cache SDKs JenkinsScripts cmake junit_family=legacy log_format=%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) +addopts='--tb=short' '--show-capture=log' # primary suite markers which should appear on every filterable test and be mutually exclusive: markers = SUITE_smoke: Tiny, quick tests of fundamental operation (tests with no suite marker will also execute here in CI) diff --git a/python/get_python_path.bat b/python/get_python_path.bat new file mode 100644 index 0000000000..66942efd70 --- /dev/null +++ b/python/get_python_path.bat @@ -0,0 +1,33 @@ +@echo off +REM +REM Copyright (c) Contributors to the Open 3D Engine Project. +REM For complete copyright and license terms please see the LICENSE at the root of this distribution. +REM +REM SPDX-License-Identifier: Apache-2.0 OR MIT +REM +REM + +:: Retreives the path to the O3DE python executable(s) + +:: Skip initialization if already completed +IF "%O3DE_PYTHONHOME_INIT%"=="1" GOTO :END_OF_FILE + +SET CMD_DIR=%~dp0 + +:: Note, many DCC tools (like Maya) include thier own versioned python interpretter. +:: Some apps may not operate correctly if PYTHONHOME is set/propogated. +:: This is definitely the case with Maya, doing so causes Maya to not boot. +FOR /F "tokens=* USEBACKQ" %%F IN (`%CMD_DIR%\python.cmd %CMD_DIR%\get_python_path.py`) DO (SET O3DE_PYTHONHOME=%%F) +echo O3DE_PYTHONHOME - is now the folder containing O3DE python executable +echo O3DE_PYTHONHOME = %O3DE_PYTHONHOME% + +SET PYTHON=%O3DE_PYTHONHOME%\python.exe + +:: Set flag so we don't initialize dccsi environment twice +SET O3DE_PYTHONHOME_INIT=1 +GOTO END_OF_FILE + +:: Return to starting directory +POPD + +:END_OF_FILE diff --git a/python/get_python_path.py b/python/get_python_path.py new file mode 100644 index 0000000000..fc503ae4ab --- /dev/null +++ b/python/get_python_path.py @@ -0,0 +1,16 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- +"""retreive the O3DE python path""" +import sys +from pathlib import Path +py_exe = Path(sys.executable) +py_dir = py_exe.parents[0] +print(py_dir.resolve()) \ No newline at end of file diff --git a/scripts/build/Platform/Linux/build_config.json b/scripts/build/Platform/Linux/build_config.json index ee4c27b0a9..b76a950beb 100644 --- a/scripts/build/Platform/Linux/build_config.json +++ b/scripts/build/Platform/Linux/build_config.json @@ -83,7 +83,7 @@ "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=TRUE -DLY_PARALLEL_LINK_JOBS=4", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "all", - "CTEST_OPTIONS": "-E Gem::EMotionFX.Editor.Tests -LE (SUITE_sandbox|SUITE_awsi) -L FRAMEWORK_googletest", + "CTEST_OPTIONS": "-E Gem::EMotionFX.Editor.Tests -LE (SUITE_sandbox|SUITE_awsi) -L FRAMEWORK_googletest --no-tests=error", "TEST_RESULTS": "True" } }, @@ -96,7 +96,7 @@ "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=FALSE -DLY_PARALLEL_LINK_JOBS=4", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "all", - "CTEST_OPTIONS": "-E Gem::EMotionFX.Editor.Tests -LE (SUITE_sandbox|SUITE_awsi) -L FRAMEWORK_googletest", + "CTEST_OPTIONS": "-E Gem::EMotionFX.Editor.Tests -LE (SUITE_sandbox|SUITE_awsi) -L FRAMEWORK_googletest --no-tests=error", "TEST_RESULTS": "True" } }, @@ -145,7 +145,7 @@ "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=TRUE -DLY_PARALLEL_LINK_JOBS=4", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_periodic", - "CTEST_OPTIONS": "-L (SUITE_periodic)", + "CTEST_OPTIONS": "-L (SUITE_periodic) --no-tests=error", "TEST_RESULTS": "True" } }, @@ -165,7 +165,7 @@ "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=TRUE -DLY_PARALLEL_LINK_JOBS=4 -DO3DE_HOME_PATH=\"${WORKSPACE}/home\" -DO3DE_REGISTER_ENGINE_PATH=\"${WORKSPACE}/o3de\" -DO3DE_REGISTER_THIS_ENGINE=TRUE", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "all", - "CTEST_OPTIONS": "-L (SUITE_sandbox)" + "CTEST_OPTIONS": "-L (SUITE_sandbox) --no-tests=error" } }, "benchmark_test_profile": { @@ -181,7 +181,7 @@ "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=TRUE -DLY_PARALLEL_LINK_JOBS=4", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_benchmark", - "CTEST_OPTIONS": "-L (SUITE_benchmark)", + "CTEST_OPTIONS": "-L (SUITE_benchmark) --no-tests=error", "TEST_RESULTS": "True" } }, diff --git a/scripts/build/Platform/Windows/build_config.json b/scripts/build/Platform/Windows/build_config.json index 7c39661273..de83294639 100644 --- a/scripts/build/Platform/Windows/build_config.json +++ b/scripts/build/Platform/Windows/build_config.json @@ -117,7 +117,7 @@ "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_smoke TEST_SUITE_main", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", - "CTEST_OPTIONS": "-L \"(SUITE_smoke|SUITE_main)\" -LE \"(REQUIRES_gpu)\" -T Test", + "CTEST_OPTIONS": "-L \"(SUITE_smoke|SUITE_main)\" -LE \"(REQUIRES_gpu)\" -T Test --no-tests=error", "TEST_METRICS": "True", "TEST_RESULTS": "True" } @@ -166,7 +166,7 @@ "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_smoke TEST_SUITE_main", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", - "CTEST_OPTIONS": "-L \"(SUITE_smoke|SUITE_main)\" -LE \"(REQUIRES_gpu)\" -T Test", + "CTEST_OPTIONS": "-L \"(SUITE_smoke|SUITE_main)\" -LE \"(REQUIRES_gpu)\" -T Test --no-tests=error", "TEST_METRICS": "True", "TEST_RESULTS": "True" } @@ -187,7 +187,7 @@ "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_smoke TEST_SUITE_main", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", - "CTEST_OPTIONS": "-L \"(SUITE_smoke_REQUIRES_gpu|SUITE_main_REQUIRES_gpu)\" -T Test", + "CTEST_OPTIONS": "-L \"(SUITE_smoke_REQUIRES_gpu|SUITE_main_REQUIRES_gpu)\" -T Test --no-tests=error", "TEST_METRICS": "True", "TEST_RESULTS": "True", "TEST_SCREENSHOTS": "True" @@ -238,7 +238,7 @@ "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_awsi", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", - "CTEST_OPTIONS": "-L \"(SUITE_awsi)\" -T Test", + "CTEST_OPTIONS": "-L \"(SUITE_awsi)\" -T Test --no-tests=error", "TEST_METRICS": "True", "TEST_RESULTS": "True" } @@ -257,7 +257,7 @@ "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_periodic", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", - "CTEST_OPTIONS": "-L \"(SUITE_periodic)\" -T Test", + "CTEST_OPTIONS": "-L \"(SUITE_periodic)\" -T Test --no-tests=error", "TEST_METRICS": "True", "TEST_RESULTS": "True" } @@ -279,7 +279,7 @@ "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_sandbox", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", - "CTEST_OPTIONS": "-L \"(SUITE_sandbox)\" -T Test", + "CTEST_OPTIONS": "-L \"(SUITE_sandbox)\" -T Test --no-tests=error", "TEST_METRICS": "True", "TEST_RESULTS": "True" } @@ -298,7 +298,7 @@ "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_benchmark", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", - "CTEST_OPTIONS": "-L \"(SUITE_benchmark)\" -T Test", + "CTEST_OPTIONS": "-L \"(SUITE_benchmark)\" -T Test --no-tests=error", "TEST_METRICS": "True", "TEST_RESULTS": "True" } diff --git a/scripts/o3de/o3de/download.py b/scripts/o3de/o3de/download.py index f8868dd460..98f5d051a4 100644 --- a/scripts/o3de/o3de/download.py +++ b/scripts/o3de/o3de/download.py @@ -90,7 +90,8 @@ def get_downloadable(engine_name: str = None, def download_o3de_object(object_name: str, default_folder_name: str, dest_path: str or pathlib.Path, - object_type: str, downloadable_kwarg_key, skip_auto_register: bool) -> int: + object_type: str, downloadable_kwarg_key, skip_auto_register: bool, + download_progress_callback = None) -> int: download_path = manifest.get_o3de_cache_folder() / default_folder_name / object_name download_path.mkdir(parents=True, exist_ok=True) @@ -104,7 +105,7 @@ def download_o3de_object(object_name: str, default_folder_name: str, dest_path: origin_uri = downloadable_object_data['originuri'] parsed_uri = urllib.parse.urlparse(origin_uri) - download_zip_result = utils.download_zip_file(parsed_uri, download_zip_path) + download_zip_result = utils.download_zip_file(parsed_uri, download_zip_path, download_progress_callback) if download_zip_result != 0: return download_zip_result @@ -147,33 +148,38 @@ def download_o3de_object(object_name: str, default_folder_name: str, dest_path: def download_engine(engine_name: str, dest_path: str or pathlib.Path, - skip_auto_register: bool) -> int: - return download_o3de_object(engine_name, 'engines', dest_path, 'engine', 'engine_name', skip_auto_register) + skip_auto_register: bool, + download_progress_callback = None) -> int: + return download_o3de_object(engine_name, 'engines', dest_path, 'engine', 'engine_name', skip_auto_register, download_progress_callback) def download_project(project_name: str, dest_path: str or pathlib.Path, - skip_auto_register: bool) -> int: - return download_o3de_object(project_name, 'projects', dest_path, 'project', 'project_name', skip_auto_register) + skip_auto_register: bool, + download_progress_callback = None) -> int: + return download_o3de_object(project_name, 'projects', dest_path, 'project', 'project_name', skip_auto_register, download_progress_callback) def download_gem(gem_name: str, dest_path: str or pathlib.Path, - skip_auto_register: bool) -> int: - return download_o3de_object(gem_name, 'gems', dest_path, 'gem', 'gem_name', skip_auto_register) + skip_auto_register: bool, + download_progress_callback = None) -> int: + return download_o3de_object(gem_name, 'gems', dest_path, 'gem', 'gem_name', skip_auto_register, download_progress_callback) def download_template(template_name: str, dest_path: str or pathlib.Path, - skip_auto_register: bool) -> int: - return download_o3de_object(template_name, 'templates', dest_path, 'template', 'template_name', skip_auto_register) + skip_auto_register: bool, + download_progress_callback = None) -> int: + return download_o3de_object(template_name, 'templates', dest_path, 'template', 'template_name', skip_auto_register, download_progress_callback) def download_restricted(restricted_name: str, dest_path: str or pathlib.Path, - skip_auto_register: bool) -> int: - return download_o3de_object(restricted_name, 'restricted', dest_path, 'restricted', 'restricted_name', skip_auto_register) + skip_auto_register: bool, + download_progress_callback = None) -> int: + return download_o3de_object(restricted_name, 'restricted', dest_path, 'restricted', 'restricted_name', skip_auto_register, download_progress_callback) def _run_download(args: argparse) -> int: diff --git a/scripts/o3de/o3de/engine_template.py b/scripts/o3de/o3de/engine_template.py index cba7539aaa..e93f972301 100755 --- a/scripts/o3de/o3de/engine_template.py +++ b/scripts/o3de/o3de/engine_template.py @@ -407,15 +407,12 @@ def create_template(source_path: pathlib.Path, source_name = os.path.basename(source_path) sanitized_source_name = utils.sanitize_identifier_for_cpp(source_name) - # if no template path, error + # if no template path, use default_templates_folder path if not template_path: - logger.info(f'Template path empty. Using source name {source_name}') - template_path = pathlib.Path(source_name) - if not template_path.is_absolute(): default_templates_folder = manifest.get_registered(default_folder='templates') - template_path = default_templates_folder / template_path - logger.info(f'Template path not a full path. Using default templates folder {template_path}') - if not force and template_path.is_dir(): + template_path = default_templates_folder / source_name + logger.info(f'Template path empty. Using default templates folder {template_path}') + if not force and template_path.is_dir() and len(list(template_path.iterdir())): logger.error(f'Template path {template_path} already exists.') return 1 @@ -1105,7 +1102,7 @@ def create_from_template(destination_path: pathlib.Path, logger.error(f'Could not find the template {template_name}=>{template_path}') return 1 - # the template.json should be in the template_path, make sure it's there a nd valid + # the template.json should be in the template_path, make sure it is valid template_json = template_path / 'template.json' if not validation.valid_o3de_template_json(template_json): logger.error(f'Template json {template_path} is invalid.') @@ -1254,7 +1251,7 @@ def create_from_template(destination_path: pathlib.Path, # destination restricted path elif destination_restricted_path: if os.path.isabs(destination_restricted_path): - restricted_default_path = manifest.get_registered(default='restricted') + restricted_default_path = manifest.get_registered(default_folder='restricted') new_destination_restricted_path = restricted_default_path / destination_restricted_path logger.info(f'{destination_restricted_path} is not a full path, making it relative' f' to default restricted path = {new_destination_restricted_path}') @@ -1346,7 +1343,7 @@ def create_project(project_path: pathlib.Path, Template instantiation specialization that makes all default assumptions for a Project template instantiation, reducing the effort needed in instancing a project :param project_path: the project path, can be absolute or relative to default projects path - :param project_name: the project name, defaults to project_path basename if not provided + :param project_name: the project name, defaults to project_path basename if not provided :param template_path: the path to the template you want to instance, can be absolute or relative to default templates path :param template_name: the name the registered template you want to instance, defaults to DefaultProject, resolves template_path :param project_restricted_path: path to the projects restricted folder, can be absolute or relative to the restricted='projects' @@ -1523,13 +1520,9 @@ def create_project(project_path: pathlib.Path, if not project_path: logger.error('Project path cannot be empty.') return 1 - if not os.path.isabs(project_path): - default_projects_folder = manifest.get_registered(default_folder='projects') - new_project_path = default_projects_folder / project_path - logger.info(f'Project Path {project_path} is not a full path, we must assume its relative' - f' to default projects path = {new_project_path}') - project_path = new_project_path - if not force and os.path.isdir(project_path) and len(os.listdir(project_path)) > 0: + + project_path = project_path.resolve() + if not force and project_path.is_dir() and len(list(project_path.iterdir())): logger.error(f'Project path {project_path} already exists and is not empty.') return 1 elif not os.path.isdir(project_path): @@ -1904,14 +1897,10 @@ def create_gem(gem_path: pathlib.Path, if not gem_path: logger.error('Gem path cannot be empty.') return 1 - if not os.path.isabs(gem_path): - default_gems_folder = manifest.get_registered(default_folder='gems') - new_gem_path = default_gems_folder / gem_path - logger.info(f'Gem Path {gem_path} is not a full path, we must assume its relative' - f' to default gems path = {new_gem_path}') - gem_path = new_gem_path - if not force and os.path.isdir(gem_path): - logger.error(f'Gem path {gem_path} already exists.') + + gem_path = gem_path.resolve() + if not force and gem_path.is_dir() and len(list(gem_path.iterdir())): + logger.error(f'Gem path {gem_path} already exists and is not empty.') return 1 else: os.makedirs(gem_path, exist_ok=force) @@ -1936,16 +1925,18 @@ def create_gem(gem_path: pathlib.Path, # gem restricted path elif gem_restricted_path: if not os.path.isabs(gem_restricted_path): - default_gems_restricted_folder = manifest.get_registered(restricted_name='gems') - new_gem_restricted_path = default_gems_restricted_folder /gem_restricted_path - logger.info(f'Gem restricted path {gem_restricted_path} is not a full path, we must assume its' - f' relative to default gems restricted path = {new_gem_restricted_path}') - gem_restricted_path = new_gem_restricted_path - elif template_restricted_path: + gem_restricted_default_path = manifest.get_registered(restricted_name='gems') + if gem_restricted_default_path: + new_gem_restricted_path = gem_restricted_default_path / gem_restricted_path + logger.info(f'Gem restricted path {gem_restricted_path} is not a full path, we must assume its' + f' relative to default gems restricted path = {new_gem_restricted_path}') + gem_restricted_path = new_gem_restricted_path + else: gem_restricted_default_path = manifest.get_registered(restricted_name='gems') - logger.info(f'--gem-restricted-path is not specified, using default gem restricted path / gem name' - f' = {gem_restricted_default_path}') - gem_restricted_path = gem_restricted_default_path + if gem_restricted_default_path: + logger.info(f'--gem-restricted-path is not specified, using default / ' + f' = {gem_restricted_default_path}') + gem_restricted_path = gem_restricted_default_path / gem_name # gem restricted relative if not gem_restricted_platform_relative_path: @@ -1964,7 +1955,7 @@ def create_gem(gem_path: pathlib.Path, replacements.append(("${NameUpper}", gem_name.upper())) replacements.append(("${NameLower}", gem_name.lower())) replacements.append(("${SanitizedCppName}", sanitized_cpp_name)) - + # module id is a uuid with { and - if module_id: @@ -2244,14 +2235,14 @@ def add_args(subparsers) -> None: create_from_template_subparser = subparsers.add_parser('create-from-template') create_from_template_subparser.add_argument('-dp', '--destination-path', type=pathlib.Path, required=True, help='The path to where you want the template instantiated,' - ' can be absolute or dev root relative.' + ' can be absolute or relative to the current working directory.' 'Ex. C:/o3de/Test' 'Test = ') group = create_from_template_subparser.add_mutually_exclusive_group(required=True) group.add_argument('-tp', '--template-path', type=pathlib.Path, required=False, help='The path to the template you want to instantiate, can be absolute' - ' or dev root/Templates relative.' + ' or relative to the current working directory.' 'Ex. C:/o3de/Template/TestTemplate' 'TestTemplate = ') group.add_argument('-tn', '--template-name', type=str, required=False, @@ -2327,7 +2318,7 @@ def add_args(subparsers) -> None: create_project_subparser = subparsers.add_parser('create-project') create_project_subparser.add_argument('-pp', '--project-path', type=pathlib.Path, required=True, help='The location of the project you wish to create from the template,' - ' can be an absolute path or dev root relative.' + ' can be an absolute path or relative to the current working directory.' ' Ex. C:/o3de/TestProject' ' TestProject = if --project-name not provided') create_project_subparser.add_argument('-pn', '--project-name', type=str, required=False, @@ -2349,8 +2340,8 @@ def add_args(subparsers) -> None: group = create_project_subparser.add_mutually_exclusive_group(required=False) group.add_argument('-prp', '--project-restricted-path', type=pathlib.Path, required=False, default=None, - help='path to the projects restricted folder, can be absolute or relative' - ' to the restricted="projects"') + help='path to the projects restricted folder, can be absolute or relative to' + ' the default restricted projects directory') group.add_argument('-prn', '--project-restricted-name', type=str, required=False, default=None, help='The name of the registered projects restricted path. If supplied this will resolve' @@ -2360,7 +2351,7 @@ def add_args(subparsers) -> None: group.add_argument('-trp', '--template-restricted-path', type=pathlib.Path, required=False, default=None, help='The templates restricted path can be absolute or relative to' - ' restricted="templates"') + 'the default restricted templates directory') group.add_argument('-trn', '--template-restricted-name', type=str, required=False, default=None, help='The name of the registered templates restricted path. If supplied this will resolve' @@ -2423,7 +2414,7 @@ def add_args(subparsers) -> None: # creation of a gem from a template (like create from template but makes gem assumptions) create_gem_subparser = subparsers.add_parser('create-gem') create_gem_subparser.add_argument('-gp', '--gem-path', type=pathlib.Path, required=True, - help='The gem path, can be absolute or relative to default gems path') + help='The gem path, can be absolute or relative to the current working directory') create_gem_subparser.add_argument('-gn', '--gem-name', type=str, help='The name to use when substituting the ${Name} placeholder for the gem,' ' must be alphanumeric, ' @@ -2444,19 +2435,18 @@ def add_args(subparsers) -> None: group = create_gem_subparser.add_mutually_exclusive_group(required=False) group.add_argument('-grp', '--gem-restricted-path', type=pathlib.Path, required=False, default=None, - help='The path to the gem restricted to write to folder if any, can be' - 'absolute or dev root relative, default is dev root/restricted.') + help='The gem restricted path, can be absolute or relative to' + ' the default restricted gems directory') group.add_argument('-grn', '--gem-restricted-name', type=str, required=False, default=None, - help='The path to the gem restricted to write to folder if any, can be' - 'absolute or dev root relative, default is dev root/restricted. If supplied' - ' this will resolve the --gem-restricted-path.') + help='The name of the gem to look up the gem restricted path if any.' + 'If supplied this will resolve the --gem-restricted-path.') group = create_gem_subparser.add_mutually_exclusive_group(required=False) group.add_argument('-trp', '--template-restricted-path', type=pathlib.Path, required=False, default=None, help='The templates restricted path, can be absolute or relative to' - ' the restricted="templates"') + ' the default restricted templates directory') group.add_argument('-trn', '--template-restricted-name', type=str, required=False, default=None, help='The name of the registered templates restricted path. If supplied' diff --git a/scripts/o3de/o3de/manifest.py b/scripts/o3de/o3de/manifest.py index d9cbe92829..fc778591c4 100644 --- a/scripts/o3de/o3de/manifest.py +++ b/scripts/o3de/o3de/manifest.py @@ -41,6 +41,11 @@ def get_o3de_folder() -> pathlib.Path: o3de_folder.mkdir(parents=True, exist_ok=True) return o3de_folder +def get_o3de_user_folder() -> pathlib.Path: + o3de_user_folder = get_home_folder() / 'O3DE' + o3de_user_folder.mkdir(parents=True, exist_ok=True) + return o3de_user_folder + def get_o3de_registry_folder() -> pathlib.Path: registry_folder = get_o3de_folder() / 'Registry' @@ -67,19 +72,19 @@ def get_o3de_engines_folder() -> pathlib.Path: def get_o3de_projects_folder() -> pathlib.Path: - projects_folder = get_o3de_folder() / 'Projects' + projects_folder = get_o3de_user_folder() / 'Projects' projects_folder.mkdir(parents=True, exist_ok=True) return projects_folder def get_o3de_gems_folder() -> pathlib.Path: - gems_folder = get_o3de_folder() / 'Gems' + gems_folder = get_o3de_user_folder() / 'Gems' gems_folder.mkdir(parents=True, exist_ok=True) return gems_folder def get_o3de_templates_folder() -> pathlib.Path: - templates_folder = get_o3de_folder() / 'Templates' + templates_folder = get_o3de_user_folder() / 'Templates' templates_folder.mkdir(parents=True, exist_ok=True) return templates_folder diff --git a/scripts/o3de/o3de/register.py b/scripts/o3de/o3de/register.py index c3d201f23c..6ad54a11f3 100644 --- a/scripts/o3de/o3de/register.py +++ b/scripts/o3de/o3de/register.py @@ -467,7 +467,7 @@ def register_restricted_path(json_data: dict, def register_repo(json_data: dict, - repo_uri: str or pathlib.Path, + repo_uri: str, remove: bool = False) -> int: if not repo_uri: logger.error(f'Repo URI cannot be empty.') @@ -480,9 +480,9 @@ def register_repo(json_data: dict, while repo_uri in json_data['repos']: json_data['repos'].remove(repo_uri) else: - repo_uri = pathlib.Path(repo_uri).resolve() - while repo_uri.as_posix() in json_data['repos']: - json_data['repos'].remove(repo_uri.as_posix()) + repo_uri = pathlib.Path(repo_uri).resolve().as_posix() + while repo_uri in json_data['repos']: + json_data['repos'].remove(repo_uri) if remove: logger.warn(f'Removing repo uri {repo_uri}.') @@ -566,7 +566,7 @@ def register(engine_path: pathlib.Path = None, external_subdir_path: pathlib.Path = None, template_path: pathlib.Path = None, restricted_path: pathlib.Path = None, - repo_uri: str or pathlib.Path = None, + repo_uri: str = None, default_engines_folder: pathlib.Path = None, default_projects_folder: pathlib.Path = None, default_gems_folder: pathlib.Path = None, @@ -596,7 +596,7 @@ def register(engine_path: pathlib.Path = None, :param default_third_party_folder: default 3rd party cache folder :param external_subdir_engine_path: Path to the engine to use when registering an external subdirectory. The registration occurs in the engine.json file in this case - :param external_subdir_engine_path: Path to the project to use when registering an external subdirectory. + :param external_subdir_project_path: Path to the project to use when registering an external subdirectory. The registrations occurs in the project.json in this case :param remove: add/remove the entries :param force: force update of the engine_path for specified "engine_name" from the engine.json file @@ -641,7 +641,7 @@ def register(engine_path: pathlib.Path = None, return 1 result = result or register_restricted_path(json_data, restricted_path, remove, project_path, engine_path) - if isinstance(repo_uri, str) or isinstance(repo_uri, pathlib.PurePath): + if isinstance(repo_uri, str): if not repo_uri: logger.error(f'Repo URI cannot be empty.') return 1 diff --git a/scripts/o3de/o3de/repo.py b/scripts/o3de/o3de/repo.py index 191fdbd3e1..5c47c2bee8 100644 --- a/scripts/o3de/o3de/repo.py +++ b/scripts/o3de/o3de/repo.py @@ -92,6 +92,44 @@ def process_add_o3de_repo(file_name: str or pathlib.Path, return 0 +def get_gem_json_paths_from_cached_repo(repo_uri: str) -> list: + url = f'{repo_uri}/repo.json' + repo_sha256 = hashlib.sha256(url.encode()) + cache_folder = manifest.get_o3de_cache_folder() + cache_filename = cache_folder / str(repo_sha256.hexdigest() + '.json') + + gem_list = [] + + file_name = pathlib.Path(cache_filename).resolve() + if not file_name.is_file(): + logger.error(f'Could not find cached repo json file for {repo_uri}') + return gem_list + + with file_name.open('r') as f: + try: + repo_data = json.load(f) + except json.JSONDecodeError as e: + logger.error(f'{file_name} failed to load: {str(e)}') + return gem_list + + # Get list of gems, then add all json paths to the list if they exist in the cache + repo_gems = [] + try: + repo_gems.append((repo_data['gems'], 'gem.json')) + except KeyError: + pass + + for o3de_object_uris, manifest_json in repo_gems: + for o3de_object_uri in o3de_object_uris: + manifest_json_uri = f'{o3de_object_uri}/{manifest_json}' + manifest_json_sha256 = hashlib.sha256(manifest_json_uri.encode()) + cache_gem_json_filepath = cache_folder / str(manifest_json_sha256.hexdigest() + '.json') + if cache_gem_json_filepath.is_file(): + logger.warn(f'Could not find cached gem json file for {o3de_object_uri} in repo {repo_uri}') + gem_list.append(cache_gem_json_filepath) + + return gem_list + def refresh_repos() -> int: json_data = manifest.load_o3de_manifest() diff --git a/scripts/o3de/o3de/utils.py b/scripts/o3de/o3de/utils.py index c5be21c60f..8663502a3f 100755 --- a/scripts/o3de/o3de/utils.py +++ b/scripts/o3de/o3de/utils.py @@ -10,6 +10,7 @@ This file contains utility functions """ import sys import uuid +import os import pathlib import shutil import urllib.request @@ -19,6 +20,29 @@ import zipfile logger = logging.getLogger() logging.basicConfig() +COPY_BUFSIZE = 64 * 1024 + +def copyfileobj(fsrc, fdst, callback, length=0): + # This is functionally the same as the python shutil copyfileobj but + # allows for a callback to return the download progress in blocks and allows + # to early out to cancel the copy. + if not length: + length = COPY_BUFSIZE + + fsrc_read = fsrc.read + fdst_write = fdst.write + + copied = 0 + while True: + buf = fsrc_read(length) + if not buf: + break + fdst_write(buf) + copied += len(buf) + if callback(copied): + return 1 + return 0 + def validate_identifier(identifier: str) -> bool: """ Determine if the identifier supplied is valid. @@ -93,18 +117,29 @@ def backup_folder(folder: str or pathlib.Path) -> None: if backup_folder_name.is_dir(): renamed = True - -def download_file(parsed_uri, download_path: pathlib.Path) -> int: +def download_file(parsed_uri, download_path: pathlib.Path, download_progress_callback = None) -> int: """ :param parsed_uri: uniform resource identifier to zip file to download :param download_path: location path on disk to download file + :download_progress_callback: callback called with the download progress as a percentage, returns true to request to cancel the download """ if download_path.is_file(): logger.warn(f'File already downloaded to {download_path}.') elif parsed_uri.scheme in ['http', 'https', 'ftp', 'ftps']: with urllib.request.urlopen(parsed_uri.geturl()) as s: + download_file_size = 0 + try: + download_file_size = s.headers['content-length'] + except KeyError: + pass + def download_progress(blocks): + if download_progress_callback and download_file_size: + return download_progress_callback(int(blocks/int(download_file_size) * 100)) + return False with download_path.open('wb') as f: - shutil.copyfileobj(s, f) + download_cancelled = copyfileobj(s, f, download_progress) + if download_cancelled: + return 1 else: origin_file = pathlib.Path(parsed_uri.geturl()).resolve() if not origin_file.is_file(): @@ -114,12 +149,12 @@ def download_file(parsed_uri, download_path: pathlib.Path) -> int: return 0 -def download_zip_file(parsed_uri, download_zip_path: pathlib.Path) -> int: +def download_zip_file(parsed_uri, download_zip_path: pathlib.Path, download_progress_callback = None) -> int: """ :param parsed_uri: uniform resource identifier to zip file to download :param download_zip_path: path to output zip file """ - download_file_result = download_file(parsed_uri, download_zip_path) + download_file_result = download_file(parsed_uri, download_zip_path, download_progress_callback) if download_file_result != 0: return download_file_result