diff --git a/AutomatedTesting/Gem/Editor/Scripts/__init__.py b/AutomatedTesting/Gem/Editor/Scripts/__init__.py new file mode 100644 index 0000000000..79f8fa4422 --- /dev/null +++ b/AutomatedTesting/Gem/Editor/Scripts/__init__.py @@ -0,0 +1,10 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" diff --git a/AutomatedTesting/Gem/Editor/Scripts/bootstrap.py b/AutomatedTesting/Gem/Editor/Scripts/bootstrap.py new file mode 100644 index 0000000000..e41f9c1767 --- /dev/null +++ b/AutomatedTesting/Gem/Editor/Scripts/bootstrap.py @@ -0,0 +1,13 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" +import sys, os +sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../../PythonTests') +from PythonAssetBuilder import bootstrap_tests diff --git a/AutomatedTesting/Gem/PythonTests/CMakeLists.txt b/AutomatedTesting/Gem/PythonTests/CMakeLists.txt index b7a5f85087..c527aea98c 100644 --- a/AutomatedTesting/Gem/PythonTests/CMakeLists.txt +++ b/AutomatedTesting/Gem/PythonTests/CMakeLists.txt @@ -134,6 +134,25 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS) ) endif() +## Python Asset Builder ## +if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS) + ly_add_pytest( + NAME AutomatedTesting::PythonAssetBuilder + TEST_SUITE periodic + TEST_SERIAL + PATH ${CMAKE_CURRENT_LIST_DIR}/PythonAssetBuilder + TIMEOUT 3600 + RUNTIME_DEPENDENCIES + Legacy::Editor + Legacy::CryRenderNULL + AZ::AssetProcessor + AutomatedTesting.Assets + Gem::EditorPythonBindings.Editor + Gem::PythonAssetBuilder.Editor + COMPONENT TestTools + ) +endif() + ## Blast ## if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS) ly_add_pytest( diff --git a/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/AssetBuilder_test.py b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/AssetBuilder_test.py new file mode 100644 index 0000000000..e914182542 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/AssetBuilder_test.py @@ -0,0 +1,57 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" +# +# This launches the AssetProcessor and Editor then attempts to find the expected +# assets created by a Python Asset Builder and the output of a scene pipeline script +# +import sys +import os +import pytest +import logging +pytest.importorskip('ly_test_tools') + +import ly_test_tools.environment.file_system as file_system +import ly_test_tools.log.log_monitor +import ly_test_tools.environment.waiter as waiter + +@pytest.mark.SUITE_sandbox +@pytest.mark.parametrize('launcher_platform', ['windows_editor']) +@pytest.mark.parametrize('project', ['AutomatedTesting']) +@pytest.mark.parametrize('level', ['auto_test']) +class TestPythonAssetProcessing(object): + def test_DetectPythonCreatedAsset(self, request, editor, level, launcher_platform): + unexpected_lines = [] + expected_lines = [ + 'Mock asset exists', + 'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center.azmodel) found', + 'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_X_negative.azmodel) found', + 'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_X_positive.azmodel) found', + 'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center.azmodel) found', + 'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center.azmodel) found', + 'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center.azmodel) found', + 'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center.azmodel) found', + 'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center.azmodel) found' + ] + timeout = 180 + halt_on_unexpected = False + test_directory = os.path.join(os.path.dirname(__file__)) + testFile = os.path.join(test_directory, 'AssetBuilder_test_case.py') + editor.args.extend(['-NullRenderer', "--skipWelcomeScreenDialog", "--autotest_mode", "--runpythontest", testFile]) + + with editor.start(): + editorlog_file = os.path.join(editor.workspace.paths.project_log(), 'Editor.log') + log_monitor = ly_test_tools.log.log_monitor.LogMonitor(editor, editorlog_file) + waiter.wait_for( + lambda: editor.is_alive(), + timeout, + exc=("Log file '{}' was never opened by another process.".format(editorlog_file)), + interval=1) + log_monitor.monitor_log_for_lines(expected_lines, unexpected_lines, halt_on_unexpected, timeout) diff --git a/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/AssetBuilder_test_case.py b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/AssetBuilder_test_case.py new file mode 100644 index 0000000000..54857c2067 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/AssetBuilder_test_case.py @@ -0,0 +1,52 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" +import azlmbr.bus +import azlmbr.asset +import azlmbr.editor +import azlmbr.math +import azlmbr.legacy.general + +def raise_and_stop(msg): + print (msg) + azlmbr.editor.EditorToolsApplicationRequestBus(azlmbr.bus.Broadcast, 'ExitNoPrompt') + +# These tests are meant to check that the test_asset.mock source asset turned into +# a test_asset.mock_asset product asset via the Python asset builder system +mockAssetType = azlmbr.math.Uuid_CreateString('{9274AD17-3212-4651-9F3B-7DCCB080E467}', 0) +mockAssetPath = 'gem/pythontests/pythonassetbuilder/test_asset.mock_asset' +assetId = azlmbr.asset.AssetCatalogRequestBus(azlmbr.bus.Broadcast, 'GetAssetIdByPath', mockAssetPath, mockAssetType, False) +if (assetId.is_valid() is False): + raise_and_stop(f'Mock AssetId is not valid! Got {assetId.to_string()} instead') + +if (assetId.to_string().endswith(':54c06b89') is False): + raise_and_stop(f'Mock AssetId has unexpected sub-id for {mockAssetPath}!') + +print ('Mock asset exists') + +# These tests detect if the geom_group.fbx file turns into a number of azmodel product assets +def test_azmodel_product(generatedModelAssetPath, expectedSubId): + azModelAssetType = azlmbr.math.Uuid_CreateString('{2C7477B6-69C5-45BE-8163-BCD6A275B6D8}', 0) + assetId = azlmbr.asset.AssetCatalogRequestBus(azlmbr.bus.Broadcast, 'GetAssetIdByPath', generatedModelAssetPath, azModelAssetType, False) + assetIdString = assetId.to_string() + if (assetIdString.endswith(':' + expectedSubId) is False): + raise_and_stop(f'Asset has unexpected asset ID ({assetIdString}) for ({generatedModelAssetPath})!') + else: + print(f'Expected subId for asset ({generatedModelAssetPath}) found') + +test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center.azmodel', '10412075') +test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_X_positive.azmodel', '10d16e68') +test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_X_negative.azmodel', '10a71973') +test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_Y_positive.azmodel', '10130556') +test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_Y_negative.azmodel', '1065724d') +test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_Z_positive.azmodel', '1024be55') +test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_Z_negative.azmodel', '1052c94e') + +azlmbr.editor.EditorToolsApplicationRequestBus(azlmbr.bus.Broadcast, 'ExitNoPrompt') diff --git a/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/__init__.py b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/__init__.py new file mode 100644 index 0000000000..6ed3dc4bda --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/__init__.py @@ -0,0 +1,10 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/bootstrap_tests.py b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/bootstrap_tests.py new file mode 100644 index 0000000000..9e7b738a4d --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/bootstrap_tests.py @@ -0,0 +1,17 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" +import os +import sys +try: + sys.path.append(os.path.dirname(os.path.abspath(__file__))) + import mock_asset_builder +except: + print ('skipping asset builder testing via mock_asset_builder') diff --git a/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/export_chunks_builder.py b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/export_chunks_builder.py new file mode 100644 index 0000000000..ad68a486b1 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/export_chunks_builder.py @@ -0,0 +1,88 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" +import uuid, os +import azlmbr.scene as sceneApi +import azlmbr.scene.graph +from scene_api import scene_data as sceneData + +def get_mesh_node_names(sceneGraph): + meshDataList = [] + node = sceneGraph.get_root() + children = [] + + while node.IsValid(): + # store children to process after siblings + if sceneGraph.has_node_child(node): + children.append(sceneGraph.get_node_child(node)) + + # store any node that has mesh data content + nodeContent = sceneGraph.get_node_content(node) + if nodeContent is not None and nodeContent.CastWithTypeName('MeshData'): + if sceneGraph.is_node_end_point(node) is False: + meshDataList.append(sceneData.SceneGraphName(sceneGraph.get_node_name(node))) + + # advance to next node + if sceneGraph.has_node_sibling(node): + node = sceneGraph.get_node_sibling(node) + elif children: + node = children.pop() + else: + node = azlmbr.scene.graph.NodeIndex() + + return meshDataList + +def update_manifest(scene): + graph = sceneData.SceneGraph(scene.graph) + meshNameList = get_mesh_node_names(graph) + sceneManifest = sceneData.SceneManifest() + sourceFilenameOnly = os.path.basename(scene.sourceFilename) + sourceFilenameOnly = sourceFilenameOnly.replace('.','_') + + for activeMeshIndex in range(len(meshNameList)): + chunkName = meshNameList[activeMeshIndex] + chunkPath = chunkName.get_path() + meshGroupName = '{}_{}'.format(sourceFilenameOnly, chunkName.get_name()) + meshGroup = sceneManifest.add_mesh_group(meshGroupName) + meshGroup['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, sourceFilenameOnly + chunkPath)) + '}' + sceneManifest.mesh_group_add_comment(meshGroup, 'auto generated by scene manifest') + sceneManifest.mesh_group_add_advanced_coordinate_system(meshGroup, None, None, None, 1.0) + + # create selection node list + pathSet = set() + for meshIndex in range(len(meshNameList)): + targetPath = meshNameList[meshIndex].get_path() + if (activeMeshIndex == meshIndex): + sceneManifest.mesh_group_select_node(meshGroup, targetPath) + else: + if targetPath not in pathSet: + pathSet.update(targetPath) + sceneManifest.mesh_group_unselect_node(meshGroup, targetPath) + + return sceneManifest.export() + +mySceneJobHandler = None + +def on_update_manifest(args): + scene = args[0] + result = update_manifest(scene) + global mySceneJobHandler + mySceneJobHandler.disconnect() + mySceneJobHandler = None + return result + +def main(): + global mySceneJobHandler + mySceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler() + mySceneJobHandler.connect() + mySceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest) + +if __name__ == "__main__": + main() diff --git a/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/geom_group.fbx b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/geom_group.fbx new file mode 100644 index 0000000000..8945a5505a --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/geom_group.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66d38948309ef273adf74b63eaa38f8fc2e2bdfbab3933d2ee082ce6a8cb108e +size 30496 diff --git a/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/geom_group.fbx.assetinfo b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/geom_group.fbx.assetinfo new file mode 100644 index 0000000000..707c6f3705 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/geom_group.fbx.assetinfo @@ -0,0 +1,9 @@ +{ + "values": + [ + { + "$type": "ScriptProcessorRule", + "scriptFilename": "Gem/PythonTests/PythonAssetBuilder/export_chunks_builder.py" + } + ] +} diff --git a/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/mock_asset_builder.py b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/mock_asset_builder.py new file mode 100644 index 0000000000..3b695e3ccd --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/mock_asset_builder.py @@ -0,0 +1,121 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" +import azlmbr.asset +import azlmbr.asset.builder +import azlmbr.bus +import azlmbr.math +import os, traceback, binascii, sys + +jobKeyName = 'Mock Asset' + +def log_exception_traceback(): + exc_type, exc_value, exc_tb = sys.exc_info() + data = traceback.format_exception(exc_type, exc_value, exc_tb) + print(str(data)) + +# creates a single job to compile for each platform +def create_jobs(request): + # create job descriptor for each platform + jobDescriptorList = [] + for platformInfo in request.enabledPlatforms: + jobDesc = azlmbr.asset.builder.JobDescriptor() + jobDesc.jobKey = jobKeyName + jobDesc.set_platform_identifier(platformInfo.identifier) + jobDescriptorList.append(jobDesc) + + response = azlmbr.asset.builder.CreateJobsResponse() + response.result = azlmbr.asset.builder.CreateJobsResponse_ResultSuccess + response.createJobOutputs = jobDescriptorList + return response + +def on_create_jobs(args): + try: + request = args[0] + return create_jobs(request) + except: + log_exception_traceback() + # returing back a default CreateJobsResponse() records an asset error + return azlmbr.asset.builder.CreateJobsResponse() + +def process_file(request): + # prepare output folder + basePath, _ = os.path.split(request.sourceFile) + outputPath = os.path.join(request.tempDirPath, basePath) + os.makedirs(outputPath, exist_ok=True) + + # write out a mock file + basePath, sourceFile = os.path.split(request.sourceFile) + mockFilename = os.path.splitext(sourceFile)[0] + '.mock_asset' + mockFilename = os.path.join(basePath, mockFilename) + mockFilename = mockFilename.replace('\\', '/') + tempFilename = os.path.join(request.tempDirPath, mockFilename) + + # write out a tempFilename like a JSON + fileOutput = open(tempFilename, "w") + fileOutput.write('{}') + fileOutput.close() + + # generate a product asset file entry + subId = binascii.crc32(mockFilename.encode()) + mockAssetType = azlmbr.math.Uuid_CreateString('{9274AD17-3212-4651-9F3B-7DCCB080E467}', 0) + product = azlmbr.asset.builder.JobProduct(mockFilename, mockAssetType, subId) + product.dependenciesHandled = True + productOutputs = [] + productOutputs.append(product) + + # fill out response object + response = azlmbr.asset.builder.ProcessJobResponse() + response.outputProducts = productOutputs + response.resultCode = azlmbr.asset.builder.ProcessJobResponse_Success + response.dependenciesHandled = True + return response + +# using the incoming 'request' find the type of job via 'jobKey' to determine what to do +def on_process_job(args): + try: + request = args[0] + if (request.jobDescription.jobKey.startswith(jobKeyName)): + return process_file(request) + except: + log_exception_traceback() + # returning back an empty ProcessJobResponse() will record an error + return azlmbr.asset.builder.ProcessJobResponse() + +# register asset builder +def register_asset_builder(busId): + assetPattern = azlmbr.asset.builder.AssetBuilderPattern() + assetPattern.pattern = '*.mock' + assetPattern.type = azlmbr.asset.builder.AssetBuilderPattern_Wildcard + + builderDescriptor = azlmbr.asset.builder.AssetBuilderDesc() + builderDescriptor.name = "Mock Builder" + builderDescriptor.patterns = [assetPattern] + builderDescriptor.busId = busId + builderDescriptor.version = 1 + + outcome = azlmbr.asset.builder.PythonAssetBuilderRequestBus(azlmbr.bus.Broadcast, 'RegisterAssetBuilder', builderDescriptor) + if outcome.IsSuccess(): + # created the asset builder to hook into the notification bus + handler = azlmbr.asset.builder.PythonBuilderNotificationBusHandler() + handler.connect(busId) + handler.add_callback('OnCreateJobsRequest', on_create_jobs) + handler.add_callback('OnProcessJobRequest', on_process_job) + return handler + +# create the asset builder handler +busIdString = '{CF5C74C1-9ED4-5851-95B1-0B15090DBEC7}' +busId = azlmbr.math.Uuid_CreateString(busIdString, 0) +handler = None +try: + handler = register_asset_builder(busId) +except: + handler = None + log_exception_traceback() diff --git a/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/test_asset.mock b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/test_asset.mock new file mode 100644 index 0000000000..6d6a52e643 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/test_asset.mock @@ -0,0 +1 @@ +mock data \ No newline at end of file diff --git a/AutomatedTesting/LightingPresets/greenwich_park_02.lightingconfig.json b/AutomatedTesting/LightingPresets/greenwich_park_02.lightingconfig.json index 1646dd18a2..bfb92bbb6f 100644 --- a/AutomatedTesting/LightingPresets/greenwich_park_02.lightingconfig.json +++ b/AutomatedTesting/LightingPresets/greenwich_park_02.lightingconfig.json @@ -1,7 +1,6 @@ { "configurations": [ { - "autoSelect": false, "displayName": "Greenwich Park 02", "skyboxImageAsset": { "assetId": { @@ -64,7 +63,6 @@ "shadowCatcherOpacity": 0.20000000298023225 }, { - "autoSelect": false, "displayName": "Greenwich Park 02 (Alt)", "skyboxImageAsset": { "assetId": { diff --git a/Code/Framework/AzCore/AzCore/Settings/SettingsRegistryMergeUtils.cpp b/Code/Framework/AzCore/AzCore/Settings/SettingsRegistryMergeUtils.cpp index 52085757fe..27f6f222dd 100644 --- a/Code/Framework/AzCore/AzCore/Settings/SettingsRegistryMergeUtils.cpp +++ b/Code/Framework/AzCore/AzCore/Settings/SettingsRegistryMergeUtils.cpp @@ -506,6 +506,7 @@ namespace AZ::SettingsRegistryMergeUtils void MergeSettingsToRegistry_AddRuntimeFilePaths(SettingsRegistryInterface& registry) { + using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString; // Binary folder AZ::IO::FixedMaxPath path = AZ::Utils::GetExecutableDirectory(); registry.Set(FilePathKey_BinaryFolder, path.LexicallyNormal().Native()); @@ -514,28 +515,25 @@ namespace AZ::SettingsRegistryMergeUtils AZ::IO::FixedMaxPath engineRoot = FindEngineRoot(registry); registry.Set(FilePathKey_EngineRootFolder, engineRoot.LexicallyNormal().Native()); - constexpr size_t bufferSize = 64; - auto buffer = AZStd::fixed_string::format("%s/project_path", BootstrapSettingsRootKey); - - AZ::SettingsRegistryInterface::FixedValueString projectPathKey(buffer); + auto projectPathKey = FixedValueString::format("%s/project_path", BootstrapSettingsRootKey); SettingsRegistryInterface::FixedValueString projectPathValue; if (registry.Get(projectPathValue, projectPathKey)) { // Cache folder // Get the name of the asset platform assigned by the bootstrap. First check for platform version such as "windows_assets" // and if that's missing just get "assets". - constexpr char platformName[] = AZ_TRAIT_OS_PLATFORM_CODENAME_LOWER; - - buffer = AZStd::fixed_string::format("%s/%s_assets", BootstrapSettingsRootKey, platformName); - AZStd::string_view assetPlatformKey(buffer); - // Use the platform codename to retrieve the default asset platform value - SettingsRegistryInterface::FixedValueString assetPlatform = AZ::OSPlatformToDefaultAssetPlatform(AZ_TRAIT_OS_PLATFORM_CODENAME); - if (!registry.Get(assetPlatform, assetPlatformKey)) + FixedValueString assetPlatform; + if (auto assetPlatformKey = FixedValueString::format("%s/%s_assets", BootstrapSettingsRootKey, AZ_TRAIT_OS_PLATFORM_CODENAME_LOWER); + !registry.Get(assetPlatform, assetPlatformKey)) { - buffer = AZStd::fixed_string::format("%s/assets", BootstrapSettingsRootKey); - assetPlatformKey = AZStd::string_view(buffer); + assetPlatformKey = FixedValueString::format("%s/assets", BootstrapSettingsRootKey); registry.Get(assetPlatform, assetPlatformKey); } + if (assetPlatform.empty()) + { + // Use the platform codename to retrieve the default asset platform value + assetPlatform = AZ::OSPlatformToDefaultAssetPlatform(AZ_TRAIT_OS_PLATFORM_CODENAME); + } // Project path - corresponds to the @devassets@ alias // NOTE: Here we append to engineRoot, but if projectPathValue is absolute then engineRoot is discarded. @@ -575,8 +573,7 @@ namespace AZ::SettingsRegistryMergeUtils { // Cache: project root - no corresponding fileIO alias, but this is where the asset database lives. // A registry override is accepted using the "project_cache_path" key. - buffer = AZStd::fixed_string::format("%s/project_cache_path", BootstrapSettingsRootKey); - AZStd::string_view projectCacheRootOverrideKey(buffer); + auto projectCacheRootOverrideKey = FixedValueString::format("%s/project_cache_path", BootstrapSettingsRootKey); // Clear path to make sure that the `project_cache_path` value isn't concatenated to the project path path.clear(); if (registry.Get(path.Native(), projectCacheRootOverrideKey)) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.cpp index 6ab3625dd2..629dcd639d 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.cpp @@ -136,6 +136,48 @@ namespace AzToolsFramework return entity->GetName(); } + EntityList EntityIdListToEntityList(const EntityIdList& inputEntityIds) + { + EntityList entities; + entities.reserve(inputEntityIds.size()); + + for (AZ::EntityId entityId : inputEntityIds) + { + if (!entityId.IsValid()) + { + continue; + } + + if (auto entity = GetEntityById(entityId)) + { + entities.emplace_back(entity); + } + } + + return entities; + } + + EntityList EntityIdSetToEntityList(const EntityIdSet& inputEntityIds) + { + EntityList entities; + entities.reserve(inputEntityIds.size()); + + for (AZ::EntityId entityId : inputEntityIds) + { + if (!entityId.IsValid()) + { + continue; + } + + if (auto entity = GetEntityById(entityId)) + { + entities.emplace_back(entity); + } + } + + return entities; + } + void GetAllComponentsForEntity(const AZ::Entity* entity, AZ::Entity::ComponentArrayType& componentsOnEntity) { if (entity) @@ -1068,6 +1110,45 @@ namespace AzToolsFramework return !allEntityClonesContainer.m_entities.empty(); } + EntityIdSet GetCulledEntityHierarchy(const EntityIdList& entities) + { + EntityIdSet culledEntities; + + for (const AZ::EntityId& entityId : entities) + { + bool selectionIncludesTransformHeritage = false; + AZ::EntityId parentEntityId = entityId; + do + { + AZ::EntityId nextParentId; + AZ::TransformBus::EventResult( + /*result*/ nextParentId, + /*address*/ parentEntityId, + &AZ::TransformBus::Events::GetParentId); + parentEntityId = nextParentId; + if (!parentEntityId.IsValid()) + { + break; + } + for (const AZ::EntityId& parentCheck : entities) + { + if (parentCheck == parentEntityId) + { + selectionIncludesTransformHeritage = true; + break; + } + } + } while (parentEntityId.IsValid() && !selectionIncludesTransformHeritage); + + if (!selectionIncludesTransformHeritage) + { + culledEntities.insert(entityId); + } + } + + return culledEntities; + } + namespace Internal { void CloneSliceEntitiesAndChildren( diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.h index 39f4c37c29..5454b19f7a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityHelpers.h @@ -47,6 +47,9 @@ namespace AzToolsFramework AZStd::string GetEntityName(const AZ::EntityId& entityId, const AZStd::string_view& nameOverride = {}); + EntityList EntityIdListToEntityList(const EntityIdList& inputEntityIds); + EntityList EntityIdSetToEntityList(const EntityIdSet& inputEntityIds); + template struct AddComponents { @@ -202,4 +205,8 @@ namespace AzToolsFramework /// Wrap EBus SetSelectedEntities call. void SelectEntities(const AzToolsFramework::EntityIdList& entities); + /// Return a set of entities, culling any that have an ancestor in the list. + /// e.g. This is useful for getting a concise set of entities that need to be duplicated. + EntityIdSet GetCulledEntityHierarchy(const EntityIdList& entities); + }; // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index 26191c97af..c64a9491c5 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -61,8 +61,7 @@ namespace AzToolsFramework PrefabOperationResult PrefabPublicHandler::CreatePrefab(const AZStd::vector& entityIds, AZ::IO::PathView filePath) { // Retrieve entityList from entityIds - EntityList inputEntityList; - EntityIdListToEntityList(entityIds, inputEntityList); + EntityList inputEntityList = EntityIdListToEntityList(entityIds); // Find common root and top level entities bool entitiesHaveCommonRoot = false; @@ -419,8 +418,7 @@ namespace AzToolsFramework InstanceOptionalReference instance = GetOwnerInstanceByEntityId(entityIds[0]); // Retrieve entityList from entityIds - EntityList inputEntityList; - EntityIdListToEntityList(entityIds, inputEntityList); + EntityList inputEntityList = EntityIdListToEntityList(entityIds); AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework); @@ -767,18 +765,5 @@ namespace AzToolsFramework return true; } - - void PrefabPublicHandler::EntityIdListToEntityList(const EntityIdList& inputEntityIds, EntityList& outEntities) - { - outEntities.reserve(inputEntityIds.size()); - - for (AZ::EntityId entityId : inputEntityIds) - { - if (entityId.IsValid()) - { - outEntities.emplace_back(GetEntityById(entityId)); - } - } - } } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h index 80f28d7edb..d9dba2a32e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h @@ -70,7 +70,6 @@ namespace AzToolsFramework static Instance* GetParentInstance(Instance* instance); static Instance* GetAncestorOfInstanceThatIsChildOfRoot(const Instance* ancestor, Instance* descendant); static void GenerateContainerEntityTransform(const EntityList& topLevelEntities, AZ::Vector3& translation, AZ::Quaternion& rotation); - static void EntityIdListToEntityList(const EntityIdList& inputEntityIds, EntityList& outEntities); InstanceEntityMapperInterface* m_instanceEntityMapperInterface = nullptr; InstanceToTemplateInterface* m_instanceToTemplateInterface = nullptr; diff --git a/Code/Framework/AzToolsFramework/Tests/Entity/EditorEntityHelpersTests.cpp b/Code/Framework/AzToolsFramework/Tests/Entity/EditorEntityHelpersTests.cpp new file mode 100644 index 0000000000..51feacfa2b --- /dev/null +++ b/Code/Framework/AzToolsFramework/Tests/Entity/EditorEntityHelpersTests.cpp @@ -0,0 +1,57 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#include + +#include +#include +#include + +namespace UnitTest +{ + class EditorEntityHelpersTest + : public ToolsApplicationFixture + { + void SetUpEditorFixtureImpl() override + { + m_parent1 = CreateDefaultEditorEntity("Parent1"); + m_child1 = CreateDefaultEditorEntity("Child1"); + m_child2 = CreateDefaultEditorEntity("Child2"); + m_grandChild1 = CreateDefaultEditorEntity("GrandChild1"); + m_parent2 = CreateDefaultEditorEntity("Parent2"); + + AZ::TransformBus::Event(m_child1, &AZ::TransformBus::Events::SetParent, m_parent1); + AZ::TransformBus::Event(m_child2, &AZ::TransformBus::Events::SetParent, m_parent1); + AZ::TransformBus::Event(m_grandChild1, &AZ::TransformBus::Events::SetParent, m_child1); + } + + public: + AZ::EntityId m_parent1; + AZ::EntityId m_child1; + AZ::EntityId m_child2; + AZ::EntityId m_grandChild1; + AZ::EntityId m_parent2; + }; + + TEST_F(EditorEntityHelpersTest, EditorEntityHelpersTests_GetCulledEntityHierarchy) + { + AzToolsFramework::EntityIdList testEntityIds{ m_parent1, m_child1, m_child2, m_grandChild1, m_parent2 }; + + AzToolsFramework::EntityIdSet culledSet = AzToolsFramework::GetCulledEntityHierarchy(testEntityIds); + + // There should only be two EntityIds returned (m_parent1, and m_parent2), + // since all the others should be culled out since they have a common ancestor + // in the list already + using ::testing::UnorderedElementsAre; + EXPECT_THAT(culledSet, UnorderedElementsAre(m_parent1, m_parent2)); + } +} diff --git a/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake b/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake index 7a2cd373a2..e54aa187e4 100644 --- a/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake +++ b/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake @@ -85,7 +85,9 @@ set(FILES Prefab/SpawnableSortEntitiesTestFixture.cpp Prefab/SpawnableSortEntitiesTestFixture.h Entity/EditorEntityContextComponentTests.cpp + Entity/EditorEntityHelpersTests.cpp Entity/EditorEntitySearchComponentTests.cpp + Entity/EditorEntitySelectionTests.cpp SliceStabilityTests/SliceStabilityTestFramework.h SliceStabilityTests/SliceStabilityTestFramework.cpp SliceStabilityTests/SliceStabilityCreateTests.cpp diff --git a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp index d9e3d9bb8c..9c917b25c0 100644 --- a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp +++ b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp @@ -670,9 +670,13 @@ void SandboxIntegrationManager::PopulateEditorGlobalContextMenu(QMenu* menu, con action = menu->addAction(QObject::tr("Create layer")); QObject::connect(action, &QAction::triggered, [this] { ContextMenu_NewLayer(); }); + AzToolsFramework::EntityIdList entities; + AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult( + entities, + &AzToolsFramework::ToolsApplicationRequests::GetSelectedEntities); + SetupLayerContextMenu(menu); - AzToolsFramework::EntityIdSet flattenedSelection; - GetSelectedEntitiesSetWithFlattenedHierarchy(flattenedSelection); + AzToolsFramework::EntityIdSet flattenedSelection = AzToolsFramework::GetCulledEntityHierarchy(entities); AzToolsFramework::SetupAddToLayerMenu(menu, flattenedSelection, [this] { return ContextMenu_NewLayer(); }); SetupSliceContextMenu(menu); @@ -1220,10 +1224,14 @@ void SandboxIntegrationManager::CloneSelection(bool& handled) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework); - AzToolsFramework::EntityIdSet duplicationSet; - GetSelectedEntitiesSetWithFlattenedHierarchy(duplicationSet); + AzToolsFramework::EntityIdList entities; + AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult( + entities, + &AzToolsFramework::ToolsApplicationRequests::GetSelectedEntities); - if (duplicationSet.size() > 0) + AzToolsFramework::EntityIdSet duplicationSet = AzToolsFramework::GetCulledEntityHierarchy(entities); + + if (!duplicationSet.empty()) { AZStd::unordered_set clonedEntities; handled = AzToolsFramework::CloneInstantiatedEntities(duplicationSet, clonedEntities); diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp index 2f71ebcb78..9522512619 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp @@ -42,7 +42,108 @@ namespace AZ // Downstream only supports 30 frames per second sample rate. Adjusting to 60 doubles the // length of the animations, they still play back at 30 frames per second. - const double AssImpAnimationImporter::s_defaultTimeStepSampleRate = 1.0 / 30.0; + const double AssImpAnimationImporter::s_defaultTimeStepBetweenFrames = 1.0 / 30.0; + + AZ::u32 GetNumKeyFrames(AZ::u32 keysSize, double duration, double ticksPerSecond) + { + if (AZ::IsClose(ticksPerSecond, 0)) + { + AZ_Warning("AnimationImporter", false, "Animation ticks per second should not be zero, defaulting to %d keyframes for animation.", keysSize); + return keysSize; + } + const double totalTicks = duration / ticksPerSecond; + AZ::u32 numKeys = keysSize; + // +1 because the animation is from [0, duration] - we have a keyframe at the end of the duration which needs to be included + double totalFramesAtDefaultTimeStep = totalTicks / AssImpAnimationImporter::s_defaultTimeStepBetweenFrames + 1; + if (!AZ::IsClose(totalFramesAtDefaultTimeStep, numKeys, 1)) + { + numKeys = AZStd::ceilf(totalFramesAtDefaultTimeStep); + } + return numKeys; + } + + double GetTimeForFrame(AZ::u32 frame, double ticksPerSecond) + { + return frame * AssImpAnimationImporter::s_defaultTimeStepBetweenFrames * ticksPerSecond; + } + + // Helper class to store key data, when translating from AssImp layout to the engine's scene format. + struct KeyData + { + KeyData(float value, float time) : + mValue(value), + mTime(time) + { + + } + + bool operator<(const KeyData& other) const + { + return mTime < other.mTime; + } + + float mValue = 0; + float mTime = 0; + }; + + template + void LerpTemplate(T& start, const T& end, float t) + { + start = start * (1.0f - t) + end * t; + } + + template<> + void LerpTemplate(aiQuaternion& start, const aiQuaternion& end, float t) + { + aiQuaternion::Interpolate(start, start, end, t); + } + + template<> + void LerpTemplate(float& start, const float& end, float t) + { + start = AZ::Lerp(start, end, t); + } + + template + bool SampleKeyFrame(FrameValueType& result, const KeyContainerType& keys, AZ::u32 numKeys, double time, AZ::u32& lastIndex) + { + if (numKeys == 0) + { + AZ_Error("AnimationImporter", numKeys > 0, "Animation key set must have at least 1 key"); + return false; + } + if (numKeys == 1) + { + result = keys[0].mValue; + return true; + } + + while (lastIndex < numKeys - 1 && time >= keys[lastIndex + 1].mTime) + { + ++lastIndex; + } + result = keys[lastIndex].mValue; + if (lastIndex < numKeys - 1) + { + auto nextValue = keys[lastIndex + 1].mValue; + float normalizedTimeBetweenFrames = 0; + if (keys[lastIndex + 1].mTime != keys[lastIndex].mTime) + { + normalizedTimeBetweenFrames = + (time - keys[lastIndex].mTime) / (keys[lastIndex + 1].mTime - keys[lastIndex].mTime); + } + else + { + AZ_Warning("AnimationImporter", false, + "Animation has keys with duplicate time %5.5f, at indices %d and %d. The second will be ignored.", + keys[lastIndex].mTime, + lastIndex, + lastIndex + 1); + } + LerpTemplate(result, nextValue, normalizedTimeBetweenFrames); + } + return true; + } AssImpAnimationImporter::AssImpAnimationImporter() { @@ -199,6 +300,14 @@ namespace AZ for (AZ::u32 animIndex = 0; animIndex < scene->mNumAnimations; ++animIndex) { const aiAnimation* animation = scene->mAnimations[animIndex]; + if (animation->mTicksPerSecond == 0) + { + AZ_Error( + "AnimationImporter", false, + "Animation name %s has a sample rate of 0 ticks per second and cannot be processed.", + animation->mName.C_Str()); + return Events::ProcessingResult::Failure; + } mapAnimationsFunc(animation->mNumChannels, animation->mChannels, animation, boneAnimations); @@ -410,70 +519,38 @@ namespace AZ anim->mNumPositionKeys, anim->mNumRotationKeys, anim->mNumScalingKeys); return Events::ProcessingResult::Failure; } - - auto sampleKeyFrame = [](const auto& keys, AZ::u32 numKeys, double time, AZ::u32& lastIndex) - { - AZ_Error("AnimationImporter", numKeys > 0, "Animation key set must have at least 1 key"); - - if (numKeys == 1) - { - return keys[0].mValue; - } - - auto returnValue = keys[0].mValue; - - for (AZ::u32 keyIndex = lastIndex; keyIndex < numKeys; ++keyIndex) - { - const auto& key = keys[keyIndex]; - lastIndex = keyIndex; - - // We want to return the key that exactly matches the time if possible, otherwise we'll keep track of the previous time - // If we don't find an exact match and end up going past the desired time (or run out of keyframes) then we return the previous key - if (key.mTime < time) - { - returnValue = key.mValue; - } - else if (AZ::IsClose(key.mTime, time)) - { - return key.mValue; - } - else - { - return returnValue; - } - } - - return returnValue; - }; // Resample the animations at a fixed time step. This matches the behaviour of // the previous SDK used. Longer term, this could be data driven, or based on the // smallest time step between key frames. // AssImp has an animation->mTicksPerSecond and animation->mDuration, but those // are less predictable than just using a fixed time step. - const double duration = animation->mDuration / animation->mTicksPerSecond; + // AssImp documentation claims animation->mDuration is the duration of the animation in ticks, but + // not all animations we've tested follow that pattern. Sometimes duration is in seconds. + const AZ::u32 numKeyFrames = GetNumKeyFrames( + AZStd::max(AZStd::max(anim->mNumScalingKeys, anim->mNumPositionKeys), anim->mNumRotationKeys), + animation->mDuration, + animation->mTicksPerSecond); - AZ::u32 numKeyFrames = AZStd::max(AZStd::max(anim->mNumScalingKeys, anim->mNumPositionKeys), anim->mNumRotationKeys); - if (!AZ::IsClose(duration / s_defaultTimeStepSampleRate, numKeyFrames, 1)) - { - double dT = duration / s_defaultTimeStepSampleRate; - numKeyFrames = AZStd::ceilf(dT) + 1; // +1 because the animation is from [0, duration] - we have a keyframe at the end of the duration which needs to be included - } - AZStd::shared_ptr createdAnimationData = AZStd::make_shared(); createdAnimationData->ReserveKeyFrames(numKeyFrames); - createdAnimationData->SetTimeStepBetweenFrames(s_defaultTimeStepSampleRate); + createdAnimationData->SetTimeStepBetweenFrames(s_defaultTimeStepBetweenFrames); AZ::u32 lastScaleIndex = 0; AZ::u32 lastPositionIndex = 0; AZ::u32 lastRotationIndex = 0; for (AZ::u32 frame = 0; frame < numKeyFrames; ++frame) { - double time = frame * s_defaultTimeStepSampleRate * animation->mTicksPerSecond; - aiVector3D scale = sampleKeyFrame(anim->mScalingKeys, anim->mNumScalingKeys, time, lastScaleIndex); - aiVector3D position = sampleKeyFrame(anim->mPositionKeys, anim->mNumPositionKeys, time, lastPositionIndex); - aiQuaternion rotation = sampleKeyFrame(anim->mRotationKeys, anim->mNumRotationKeys, time, lastRotationIndex); + const double time = GetTimeForFrame(frame, animation->mTicksPerSecond); + aiVector3D scale = aiVector3D(1.f, 1.f, 1.f), position = aiVector3D(0.f, 0.f, 0.f); + aiQuaternion rotation(1.f, 0.f, 0.f, 0.f); + if (!SampleKeyFrame(scale, anim->mScalingKeys, anim->mNumScalingKeys, time, lastScaleIndex) || + !SampleKeyFrame(position, anim->mPositionKeys, anim->mNumPositionKeys, time, lastPositionIndex) || + !SampleKeyFrame(rotation, anim->mRotationKeys, anim->mNumRotationKeys, time, lastRotationIndex)) + { + return Events::ProcessingResult::Failure; + } aiMatrix4x4 transform(scale, rotation, position); @@ -520,28 +597,6 @@ namespace AZ // SetTimeStepBetweenFrames set on the animation data // Keyframes. Weights (Values in FBX SDK) per key time. // Keyframes generated for every single frame of the animation. - - // Helper class to store key data, when translating from AssImp layout to the engine's scene format. - struct KeyData - { - KeyData(float weight, float time) : - m_weight(weight), - m_time(time) - { - - } - - bool operator<(const KeyData& other) const - { - return m_time < other.m_time; - } - - // Naming in the previous SDK (FBX SDK) and in the engine's scene format - // doesn't match AssImp's naming convention. - // weight here is the AssImp's name for the data, it was named value in FBX SDK. - float m_weight = 0; - float m_time = 0; - }; typedef AZStd::map> ValueToKeyDataMap; ValueToKeyDataMap valueToKeyDataMap; @@ -562,44 +617,27 @@ namespace AZ { AZStd::shared_ptr morphAnimNode = AZStd::make_shared(); - morphAnimNode->ReserveKeyFrames(animation->mDuration + 1); - morphAnimNode->SetTimeStepBetweenFrames(1.0 / animation->mTicksPerSecond); + + const AZ::u32 numKeyFrames = GetNumKeyFrames(keys.size(), animation->mDuration, animation->mTicksPerSecond); + morphAnimNode->ReserveKeyFrames(numKeyFrames); + morphAnimNode->SetTimeStepBetweenFrames(s_defaultTimeStepBetweenFrames); aiAnimMesh* aiAnimMesh = mesh->mAnimMeshes[meshIdx]; AZStd::string_view nodeName(aiAnimMesh->mName.C_Str()); const AZ::u32 maxKeys = keys.size(); AZ::u32 keyIdx = 0; - for (AZ::u32 time = 0; time <= animation->mDuration; ++time) + for (AZ::u32 frame = 0; frame <= numKeyFrames; ++frame) { - if (keyIdx < maxKeys - 1 && time >= keys[keyIdx+1].m_time) - { - ++keyIdx; - } - float weight_value = keys[keyIdx].m_weight; - if (keyIdx < maxKeys - 1) - { - float nextWeight = keys[keyIdx+1].m_weight; - float normalizedTimeBetweenFrames = 0; + const double time = GetTimeForFrame(frame, animation->mTicksPerSecond); - if (keys[keyIdx + 1].m_time != keys[keyIdx].m_time) - { - normalizedTimeBetweenFrames = - (time - keys[keyIdx].m_time) / (keys[keyIdx + 1].m_time - keys[keyIdx].m_time); - } - else - { - AZ_Warning("AnimationImporter", false, - "Morph target mesh %s has keys with duplicate time, at indices %d and %d. The second will be ignored.", - nodeName.data(), - keyIdx, - keyIdx+1); - } - - // AssImp and FBX both only support linear interpolation for blend shapes. - weight_value = AZ::Lerp(weight_value, nextWeight, normalizedTimeBetweenFrames); + float weight = 0; + if (!SampleKeyFrame(weight, keys, keys.size(), time, keyIdx)) + { + return Events::ProcessingResult::Failure; } - morphAnimNode->AddKeyFrame(weight_value); + + morphAnimNode->AddKeyFrame(weight); } diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.h b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.h index 72180dea1a..3c06153d04 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.h +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.h @@ -45,7 +45,7 @@ namespace AZ const aiMeshMorphAnim* meshMorphAnim, const aiMesh* mesh); - static const double s_defaultTimeStepSampleRate; + static const double s_defaultTimeStepBetweenFrames; protected: static const char* s_animationNodeName; diff --git a/Code/Tools/SceneAPI/SceneCore/Containers/SceneGraph.cpp b/Code/Tools/SceneAPI/SceneCore/Containers/SceneGraph.cpp index 934b0154c9..87c265481f 100644 --- a/Code/Tools/SceneAPI/SceneCore/Containers/SceneGraph.cpp +++ b/Code/Tools/SceneAPI/SceneCore/Containers/SceneGraph.cpp @@ -43,7 +43,7 @@ namespace AZ AZ::BehaviorContext* behaviorContext = azrtti_cast(context); if (behaviorContext) { - behaviorContext->Class() + behaviorContext->Class("NodeIndex") ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common) ->Attribute(AZ::Script::Attributes::Module, "scene.graph") ->Constructor<>() @@ -57,7 +57,7 @@ namespace AZ ->Attribute(AZ::Script::Attributes::Operator, AZ::Script::Attributes::OperatorType::ToString) ; - behaviorContext->Class() + behaviorContext->Class("SceneGraphName") ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common) ->Attribute(AZ::Script::Attributes::Module, "scene.graph") ->Constructor() diff --git a/Gems/AWSClientAuth/Code/Source/Authentication/AuthenticationProviderManager.cpp b/Gems/AWSClientAuth/Code/Source/Authentication/AuthenticationProviderManager.cpp index f6e5efd106..d4d2d0d67b 100644 --- a/Gems/AWSClientAuth/Code/Source/Authentication/AuthenticationProviderManager.cpp +++ b/Gems/AWSClientAuth/Code/Source/Authentication/AuthenticationProviderManager.cpp @@ -48,7 +48,7 @@ namespace AWSClientAuth m_settingsRegistry = AZStd::make_shared(); AZStd::array resolvedPath{}; - AZ::IO::FileIOBase::GetInstance()->ResolvePath(settingsRegistryPath.data(), resolvedPath.data(), resolvedPath.size()); + fileIO->ResolvePath(settingsRegistryPath.data(), resolvedPath.data(), resolvedPath.size()); if (!m_settingsRegistry->MergeSettingsFile(resolvedPath.data(), AZ::SettingsRegistryInterface::Format::JsonMergePatch)) diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/LightingPreset.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/LightingPreset.h index 1c220e0d8d..f1d8337a35 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/LightingPreset.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/LightingPreset.h @@ -78,7 +78,6 @@ namespace AZ AZ_CLASS_ALLOCATOR(LightingPreset, AZ::SystemAllocator, 0); static void Reflect(AZ::ReflectContext* context); - bool m_autoSelect = false; AZStd::string m_displayName; AZ::Data::Asset m_iblDiffuseImageAsset; AZ::Data::Asset m_iblSpecularImageAsset; diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/ModelPreset.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/ModelPreset.h index 896d6fb78f..9ca8c13859 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/ModelPreset.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/ModelPreset.h @@ -32,7 +32,6 @@ namespace AZ AZ_CLASS_ALLOCATOR(ModelPreset, AZ::SystemAllocator, 0); static void Reflect(AZ::ReflectContext* context); - bool m_autoSelect = false; AZStd::string m_displayName; AZ::Data::Asset m_modelAsset; AZ::Data::Asset m_previewImageAsset; diff --git a/Gems/Atom/Feature/Common/Code/Source/Utils/EditorLightingPreset.cpp b/Gems/Atom/Feature/Common/Code/Source/Utils/EditorLightingPreset.cpp index e7f9c8b09f..1da9463bd8 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Utils/EditorLightingPreset.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Utils/EditorLightingPreset.cpp @@ -112,7 +112,6 @@ namespace AZ ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->DataElement(AZ::Edit::UIHandlers::Default, &LightingPreset::m_displayName, "Display Name", "Identifier used for display and selection") - ->DataElement(AZ::Edit::UIHandlers::Default, &LightingPreset::m_autoSelect, "Auto Select", "When true, the configuration is automatically selected when loaded") ->DataElement(AZ::Edit::UIHandlers::Default, &LightingPreset::m_iblDiffuseImageAsset, "IBL Diffuse Image Asset", "IBL diffuse image asset reference") ->DataElement(AZ::Edit::UIHandlers::Default, &LightingPreset::m_iblSpecularImageAsset, "IBL Specular Image Asset", "IBL specular image asset reference") ->DataElement(AZ::Edit::UIHandlers::Default, &LightingPreset::m_skyboxImageAsset, "Skybox Image Asset", "Skybox image asset reference") diff --git a/Gems/Atom/Feature/Common/Code/Source/Utils/EditorModelPreset.cpp b/Gems/Atom/Feature/Common/Code/Source/Utils/EditorModelPreset.cpp index b8b0b610f3..bed320c45d 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Utils/EditorModelPreset.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Utils/EditorModelPreset.cpp @@ -32,7 +32,6 @@ namespace AZ ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->DataElement(AZ::Edit::UIHandlers::Default, &ModelPreset::m_displayName, "Display Name", "Identifier used for display and selection") - ->DataElement(AZ::Edit::UIHandlers::Default, &ModelPreset::m_autoSelect, "Auto Select", "When true, the configuration is automatically selected when loaded") ->DataElement(AZ::Edit::UIHandlers::Default, &ModelPreset::m_modelAsset, "Model Asset", "Model asset reference") ->DataElement(AZ::Edit::UIHandlers::Default, &ModelPreset::m_previewImageAsset, "Preview Image Asset", "Preview image asset reference") ; diff --git a/Gems/Atom/Feature/Common/Code/Source/Utils/LightingPreset.cpp b/Gems/Atom/Feature/Common/Code/Source/Utils/LightingPreset.cpp index 837a725a9f..77924b87a3 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Utils/LightingPreset.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Utils/LightingPreset.cpp @@ -105,8 +105,7 @@ namespace AZ serializeContext->RegisterGenericType>(); serializeContext->Class() - ->Version(4) - ->Field("autoSelect", &LightingPreset::m_autoSelect) + ->Version(5) ->Field("displayName", &LightingPreset::m_displayName) ->Field("iblDiffuseImageAsset", &LightingPreset::m_iblDiffuseImageAsset) ->Field("iblSpecularImageAsset", &LightingPreset::m_iblSpecularImageAsset) @@ -128,7 +127,6 @@ namespace AZ ->Attribute(AZ::Script::Attributes::Module, "render") ->Constructor() ->Constructor() - ->Property("autoSelect", BehaviorValueProperty(&LightingPreset::m_autoSelect)) ->Property("displayName", BehaviorValueProperty(&LightingPreset::m_displayName)) ->Property("alternateSkyboxImageAsset", BehaviorValueProperty(&LightingPreset::m_alternateSkyboxImageAsset)) ->Property("skyboxImageAsset", BehaviorValueProperty(&LightingPreset::m_skyboxImageAsset)) diff --git a/Gems/Atom/Feature/Common/Code/Source/Utils/ModelPreset.cpp b/Gems/Atom/Feature/Common/Code/Source/Utils/ModelPreset.cpp index e93bfa527a..2795617c18 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Utils/ModelPreset.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Utils/ModelPreset.cpp @@ -25,8 +25,7 @@ namespace AZ if (auto serializeContext = azrtti_cast(context)) { serializeContext->Class() - ->Version(2) - ->Field("autoSelect", &ModelPreset::m_autoSelect) + ->Version(3) ->Field("displayName", &ModelPreset::m_displayName) ->Field("modelAsset", &ModelPreset::m_modelAsset) ->Field("previewImageAsset", &ModelPreset::m_previewImageAsset) @@ -41,7 +40,6 @@ namespace AZ ->Attribute(AZ::Script::Attributes::Module, "render") ->Constructor() ->Constructor() - ->Property("autoSelect", BehaviorValueProperty(&ModelPreset::m_autoSelect)) ->Property("displayName", BehaviorValueProperty(&ModelPreset::m_displayName)) ->Property("modelAsset", BehaviorValueProperty(&ModelPreset::m_modelAsset)) ->Property("previewImageAsset", BehaviorValueProperty(&ModelPreset::m_previewImageAsset)) diff --git a/Gems/Atom/TestData/TestData/LightingPresets/urban_street_02.lightingpreset.azasset b/Gems/Atom/TestData/TestData/LightingPresets/urban_street_02.lightingpreset.azasset index d0a290649b..a676d5fba6 100644 --- a/Gems/Atom/TestData/TestData/LightingPresets/urban_street_02.lightingpreset.azasset +++ b/Gems/Atom/TestData/TestData/LightingPresets/urban_street_02.lightingpreset.azasset @@ -3,7 +3,6 @@ "Version": 1, "ClassName": "AZ::Render::LightingPreset", "ClassData": { - "autoSelect": false, "displayName": "Urban Street 02", "skyboxImageAsset": { "assetId": { diff --git a/Gems/Atom/Tools/MaterialEditor/Assets/MaterialEditor/LightingPresets/_TEMPLATE_.lightingconfig.json.template b/Gems/Atom/Tools/MaterialEditor/Assets/MaterialEditor/LightingPresets/_TEMPLATE_.lightingconfig.json.template index 7cfa7edbc3..6d87c5eb7b 100644 --- a/Gems/Atom/Tools/MaterialEditor/Assets/MaterialEditor/LightingPresets/_TEMPLATE_.lightingconfig.json.template +++ b/Gems/Atom/Tools/MaterialEditor/Assets/MaterialEditor/LightingPresets/_TEMPLATE_.lightingconfig.json.template @@ -2,7 +2,6 @@ "configurations": [ { "displayName": "Substance: