Merge branch 'development' of https://github.com/aws-lumberyard/o3de into Neil_P0_updates

monroegm-disable-blank-issue-2
Neil Widmaier 4 years ago
commit a67671759e

@ -73,7 +73,20 @@ def Multiplayer_AutoComponent_RPC():
helper.succeed_if_log_line_found('EditorServer', "Script: AutoComponent_RPC_NetLevelEntity: Authority sending RPC to play some fx.", section_tracer.prints, PLAYFX_RPC_WAIT_TIME_SECONDS)
helper.succeed_if_log_line_found('Script', "AutoComponent_RPC_NetLevelEntity: I'm a client playing some fx.", section_tracer.prints, PLAYFX_RPC_WAIT_TIME_SECONDS)
# Autonomous->Authority RPC
# Sending 2 RPCs: 1 containing a parameter and 1 without
AUTONOMOUS_TO_AUTHORITY_RPC_WAIT_TIME_SECONDS = 1.0 # This RPC is sent as soon as the autonomous player script is spawned. 1 second should be more than enough time to send/receive that RPC.
helper.succeed_if_log_line_found('Script', "AutoComponent_RPC: Sending AutonomousToAuthorityNoParam RPC.", section_tracer.prints, AUTONOMOUS_TO_AUTHORITY_RPC_WAIT_TIME_SECONDS)
helper.succeed_if_log_line_found('Script', "AutoComponent_RPC: Sending AutonomousToAuthority RPC (with float param).", section_tracer.prints, AUTONOMOUS_TO_AUTHORITY_RPC_WAIT_TIME_SECONDS)
helper.succeed_if_log_line_found('EditorServer', "Script: AutoComponent_RPC: Successfully received AutonomousToAuthorityNoParams RPC.", section_tracer.prints, AUTONOMOUS_TO_AUTHORITY_RPC_WAIT_TIME_SECONDS)
helper.succeed_if_log_line_found('EditorServer', "Script: AutoComponent_RPC: Successfully received AutonomousToAuthority RPC (with expected float param).", section_tracer.prints, AUTONOMOUS_TO_AUTHORITY_RPC_WAIT_TIME_SECONDS)
# Server->Authority RPC. Inter-Entity Communication.
SERVER_TO_AUTHORITY_RPC_WAIT_TIME_SECONDS = 1.0 # This RPC is sent as soon as the networked level entity finds the player in the level, and previous tests are relying on the player's existence. 1 second should be more than enough time to send/receive that RPC.
helper.succeed_if_log_line_found('EditorServer', "Script: AutoComponent_RPC_NetLevelEntity: Send ServerToAuthority RPC.", section_tracer.prints, SERVER_TO_AUTHORITY_RPC_WAIT_TIME_SECONDS)
helper.succeed_if_log_line_found('EditorServer', "Script: AutoComponent_RPC: Received ServerToAuthority RPC. Damage=42.", section_tracer.prints, SERVER_TO_AUTHORITY_RPC_WAIT_TIME_SECONDS)
# Exit game mode
helper.exit_game_mode(TestSuccessFailTuples.exit_game_mode)

@ -4,6 +4,7 @@ For complete copyright and license terms please see the LICENSE at the root of t
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
class HeightTests:
single_gradient_height_correct = (
"Successfully retrieved height for gradient1.",
@ -22,6 +23,7 @@ class HeightTests:
"OnTerrainDataChanged call count incorrect."
)
def TerrainHeightGradientList_AddRemoveGradientWorks():
"""
Summary:
@ -29,22 +31,16 @@ def TerrainHeightGradientList_AddRemoveGradientWorks():
:return: None
"""
import os
import math as sys_math
import azlmbr.legacy.general as general
import azlmbr.bus as bus
import azlmbr.math as math
import azlmbr.terrain as terrain
import azlmbr.editor as editor
import azlmbr.vegetation as vegetation
import azlmbr.entity as EntityId
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
import editor_python_test_tools.pyside_utils as pyside_utils
from editor_python_test_tools.editor_entity_utils import EditorEntity
terrain_changed_call_count = 0
expected_terrain_changed_calls = 0
@ -87,11 +83,8 @@ def TerrainHeightGradientList_AddRemoveGradientWorks():
Report.result(test_results, sys_math.isclose(height, expected_height, abs_tol=test_tolerance))
helper.init_idle()
# Open a level.
helper.open_level("Physics", "Base")
helper.wait_for_condition(lambda: general.get_current_level_name() == "Base", 2.0)
hydra.open_base_level()
general.idle_wait_frames(1)
@ -101,7 +94,7 @@ def TerrainHeightGradientList_AddRemoveGradientWorks():
aabb_height = 1024.0
box_dimensions = math.Vector3(1.0, 1.0, aabb_height);
# Create a main entity with a LayerSpawner, AAbb and HeightGradientList.
# Create a main entity with a LayerSpawner, AAbb and HeightGradientList.
main_entity = create_entity_at("entity2", [layerspawner_component_name, gradientlist_component_name, aabb_component_name], 0.0, 0.0, aabb_height/2.0)
# Create three gradient entities.
@ -138,7 +131,8 @@ def TerrainHeightGradientList_AddRemoveGradientWorks():
# Add gradient3, the height should still be the second value, as that was the highest.
set_gradients_check_height(main_entity, [gradient_entity1.id, gradient_entity2.id, gradient_entity3.id], aabb_height * gradient_values[1], HeightTests.triple_gradient_height_correct)
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(TerrainHeightGradientList_AddRemoveGradientWorks)
Report.start_test(TerrainHeightGradientList_AddRemoveGradientWorks)

@ -4,6 +4,7 @@ For complete copyright and license terms please see the LICENSE at the root of t
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
class MacroMaterialTests:
setup_test = (
"Setup successful",
@ -30,6 +31,7 @@ class MacroMaterialTests:
"Timed out waiting for OnTerrainMacroMaterialRegionChanged"
)
def TerrainMacroMaterialComponent_MacroMaterialActivates():
"""
Summary:
@ -38,7 +40,6 @@ def TerrainMacroMaterialComponent_MacroMaterialActivates():
"""
import os
import math as sys_math
import azlmbr.legacy.general as general
import azlmbr.asset as asset
@ -46,15 +47,11 @@ def TerrainMacroMaterialComponent_MacroMaterialActivates():
import azlmbr.math as math
import azlmbr.terrain as terrain
import azlmbr.editor as editor
import azlmbr.vegetation as vegetation
import azlmbr.entity as EntityId
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.utils import Report
from editor_python_test_tools.utils import TestHelper as helper
import editor_python_test_tools.pyside_utils as pyside_utils
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.asset_utils import Asset
material_created_called = False
material_changed_called = False
@ -84,11 +81,8 @@ def TerrainMacroMaterialComponent_MacroMaterialActivates():
nonlocal material_destroyed_called
material_destroyed_called = True
helper.init_idle()
# Open a level.
helper.open_level("Physics", "Base")
helper.wait_for_condition(lambda: general.get_current_level_name() == "Base", 2.0)
hydra.open_base_level()
general.idle_wait_frames(1)
@ -160,7 +154,8 @@ def TerrainMacroMaterialComponent_MacroMaterialActivates():
region_changed_call_result = helper.wait_for_condition(lambda: material_region_changed_called == True, 2.0)
Report.result(MacroMaterialTests.material_changed_call_on_aabb_change, region_changed_call_result)
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(TerrainMacroMaterialComponent_MacroMaterialActivates)
Report.start_test(TerrainMacroMaterialComponent_MacroMaterialActivates)

@ -5,8 +5,9 @@ For complete copyright and license terms please see the LICENSE at the root of t
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
#fmt: off
class Tests():
class Tests:
create_test_entity = ("Entity created successfully", "Failed to create Entity")
add_axis_aligned_box_shape = ("Axis Aligned Box Shape component added", "Failed to add Axis Aligned Box Shape component")
add_terrain_collider = ("Terrain Physics Heightfield Collider component added", "Failed to add a Terrain Physics Heightfield Collider component")
@ -14,6 +15,7 @@ class Tests():
configuration_changed = ("Terrain size changed successfully", "Failed terrain size change")
#fmt: on
def TerrainPhysicsCollider_ChangesSizeWithAxisAlignedBoxShapeChanges():
"""
Summary:
@ -36,24 +38,24 @@ def TerrainPhysicsCollider_ChangesSizeWithAxisAlignedBoxShapeChanges():
:return: None
"""
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import TestHelper as helper
from editor_python_test_tools.utils import Report, Tracer
import azlmbr.legacy.general as general
import azlmbr.physics as physics
import azlmbr.math as azmath
import azlmbr.bus as bus
import sys
import math
SET_BOX_X_SIZE = 5.0
SET_BOX_Y_SIZE = 6.0
EXPECTED_COLUMN_SIZE = SET_BOX_X_SIZE + 1
EXPECTED_ROW_SIZE = SET_BOX_Y_SIZE + 1
helper.init_idle()
# 1) Load the level
helper.open_level("", "Base")
hydra.open_base_level()
# 2) Create test entity
test_entity = EditorEntity.create_editor_entity("TestEntity")

@ -54,7 +54,6 @@ def TerrainSystem_VegetationSpawnsOnTerrainSurfaces():
"""
import os
import sys
import math as sys_math
import azlmbr.legacy.general as general
@ -62,10 +61,8 @@ def TerrainSystem_VegetationSpawnsOnTerrainSurfaces():
import azlmbr.math as math
import azlmbr.areasystem as areasystem
import azlmbr.editor as editor
import azlmbr.vegetation as vegetation
import azlmbr.terrain as terrain
import azlmbr.entity as EntityId
import azlmbr.surface_data as surface_data
import editor_python_test_tools.hydra_editor_utils as hydra
@ -86,11 +83,8 @@ def TerrainSystem_VegetationSpawnsOnTerrainSurfaces():
return highest_z, lowest_z
helper.init_idle()
# Open an empty level.
helper.open_level("Physics", "Base")
helper.wait_for_condition(lambda: general.get_current_level_name() == "Base", 2.0)
hydra.open_base_level()
general.idle_wait_frames(1)
@ -208,7 +202,8 @@ def TerrainSystem_VegetationSpawnsOnTerrainSurfaces():
Report.result(VegetationTests.testTag3_excluded_vegetation_z_correct, highest_z < box_height * gradient_value_2)
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(TerrainSystem_VegetationSpawnsOnTerrainSurfaces)
Report.start_test(TerrainSystem_VegetationSpawnsOnTerrainSurfaces)

@ -5,8 +5,9 @@ For complete copyright and license terms please see the LICENSE at the root of t
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
#fmt: off
class Tests():
class Tests:
create_terrain_spawner_entity = ("Terrain_spawner_entity created successfully", "Failed to create terrain_spawner_entity")
create_height_provider_entity = ("Height_provider_entity created successfully", "Failed to create height_provider_entity")
create_test_ball = ("Ball created successfully", "Failed to create Ball")
@ -19,6 +20,7 @@ class Tests():
no_errors_and_warnings_found = ("No errors and warnings found", "Found errors and warnings")
#fmt: on
def Terrain_SupportsPhysics():
"""
Summary:
@ -46,8 +48,7 @@ def Terrain_SupportsPhysics():
:return: None
"""
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import TestHelper as helper, Report
from editor_python_test_tools.utils import TestHelper as helper
from editor_python_test_tools.utils import Report, Tracer
import editor_python_test_tools.hydra_editor_utils as hydra
import azlmbr.math as azmath
@ -59,12 +60,9 @@ def Terrain_SupportsPhysics():
SET_BOX_X_SIZE = 1024.0
SET_BOX_Y_SIZE = 1024.0
SET_BOX_Z_SIZE = 100.0
helper.init_idle()
# 1) Load the level
helper.open_level("", "Base")
helper.wait_for_condition(lambda: general.get_current_level_name() == "Base", 2.0)
hydra.open_base_level()
#1a) Load the level components
hydra.add_level_component("Terrain World")

@ -47,8 +47,8 @@ def Terrain_World_ConfigurationWorks():
12) Check terrain does not exist at a known position outside the world
13) Check height value is the expected one when query resolution is changed
"""
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import TestHelper as helper, Report
from editor_python_test_tools.utils import TestHelper as helper
from editor_python_test_tools.utils import Report, Tracer
import editor_python_test_tools.hydra_editor_utils as hydra
import azlmbr.math as azmath
@ -62,14 +62,11 @@ def Terrain_World_ConfigurationWorks():
SET_BOX_Y_SIZE = 2048.0
SET_BOX_Z_SIZE = 100.0
CLAMP = 1
helper.init_idle()
# 1) Start the Tracer to catch any errors and warnings
with Tracer() as section_tracer:
# 2) Load the level
helper.open_level("", "Base")
helper.wait_for_condition(lambda: general.get_current_level_name() == "Base", 2.0)
hydra.open_base_level()
# 3) Load the level components
terrain_world_component = hydra.add_level_component("Terrain World")

@ -3,25 +3,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
"""
# This suite consists of all test cases that are passing and have been verified.
import pytest
import os
import sys
from ly_test_tools import LAUNCHERS
from ly_test_tools.o3de.editor_test import EditorTestSuite, EditorSharedTest
@pytest.mark.SUITE_main
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("project", ["AutomatedTesting"])
class TestAutomation(EditorTestSuite):
enable_prefab_system = False
class test_AxisAlignedBoxShape_ConfigurationWorks(EditorSharedTest):
from .EditorScripts import TerrainPhysicsCollider_ChangesSizeWithAxisAlignedBoxShapeChanges as test_module

@ -78,7 +78,7 @@ class TestAutomationBase:
file_system.restore_backup(workspace.paths.editor_log(), workspace.paths.project_log())
except FileNotFoundError as e:
self.logger.debug(f"File restoration failed, editor log could not be found.\nError: {e}")
editor.kill()
editor.stop()
request.addfinalizer(teardown)
@ -113,7 +113,7 @@ class TestAutomationBase:
editor.wait(TestAutomationBase.MAX_TIMEOUT)
except WaitTimeoutError:
errors.append(TestRunError("TIMEOUT", f"Editor did not close after {TestAutomationBase.MAX_TIMEOUT} seconds, verify the test is ending and the application didn't freeze"))
editor.kill()
editor.stop()
output = editor.get_output()
self.logger.debug("Test output:\n" + output)

@ -669,8 +669,14 @@
"Component_[7256163899440301540]": {
"$type": "EditorScriptCanvasComponent",
"Id": 7256163899440301540,
"m_name": "AutoComponent_RPC_NetLevelEntity",
"m_name": "AutoComponent_RPC_NetLevelEntity.scriptcanvas",
"runtimeDataIsValid": true,
"runtimeDataOverrides": {
"source": {
"id": "{1D517006-AC01-5ECA-AE66-0E007871F0CD}",
"path": "C:/prj/o3de/AutomatedTesting/Levels/Multiplayer/AutoComponent_RPC/AutoComponent_RPC_NetLevelEntity.scriptcanvas"
}
},
"sourceHandle": {
"id": "{1D517006-AC01-5ECA-AE66-0E007871F0CD}",
"path": "levels/multiplayer/autocomponent_rpc/autocomponent_rpc_netlevelentity.scriptcanvas"

@ -91,22 +91,10 @@
"$type": "EditorScriptCanvasComponent",
"Id": 10596595655489113153,
"m_name": "AutoComponent_RPC",
"m_assetHolder": {
"m_asset": {
"assetId": {
"guid": "{5ED120C4-07DC-56F1-80A7-37BFC98FD74E}"
},
"assetHint": "levels/multiplayer/autocomponent_rpc/autocomponent_rpc.scriptcanvas"
}
},
"runtimeDataIsValid": true,
"runtimeDataOverrides": {
"source": {
"assetId": {
"guid": "{5ED120C4-07DC-56F1-80A7-37BFC98FD74E}"
},
"assetHint": "levels/multiplayer/autocomponent_rpc/autocomponent_rpc.scriptcanvas"
}
"sourceHandle": {
"id": "{5ED120C4-07DC-56F1-80A7-37BFC98FD74E}",
"path": "levels/multiplayer/autocomponent_rpc/autocomponent_rpc.scriptcanvas"
}
},
"Component_[11440172471478606933]": {
@ -184,6 +172,13 @@
"$type": "EditorEntityIconComponent",
"Id": 9407892837096707905
},
"Component_[9505416969829669337]": {
"$type": "EditorTagComponent",
"Id": 9505416969829669337,
"Tags": [
"Player"
]
},
"Component_[9878555871810913249]": {
"$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",
"Id": 9878555871810913249,

@ -114,9 +114,7 @@ namespace UnitTest
inline static const char* Passenger2EntityName = "Passenger2";
};
// Test was disabled because the implementation of GetFocusedPrefabInstance now relies on the Prefab EOS,
// which is not used by our test environment. This can be restored once Instance handles are implemented.
TEST_F(PrefabFocusTests, DISABLED_PrefabFocus_FocusOnOwningPrefab_RootContainer)
TEST_F(PrefabFocusTests, FocusOnOwningPrefabRootContainer)
{
// Verify FocusOnOwningPrefab works when passing the container entity of the root prefab.
{
@ -131,9 +129,7 @@ namespace UnitTest
}
}
// Test was disabled because the implementation of GetFocusedPrefabInstance now relies on the Prefab EOS,
// which is not used by our test environment. This can be restored once Instance handles are implemented.
TEST_F(PrefabFocusTests, DISABLED_PrefabFocus_FocusOnOwningPrefab_RootEntity)
TEST_F(PrefabFocusTests, FocusOnOwningPrefabRootEntity)
{
// Verify FocusOnOwningPrefab works when passing a nested entity of the root prefab.
{
@ -148,7 +144,7 @@ namespace UnitTest
}
}
TEST_F(PrefabFocusTests, PrefabFocus_FocusOnOwningPrefab_NestedContainer)
TEST_F(PrefabFocusTests, FocusOnOwningPrefabNestedContainer)
{
// Verify FocusOnOwningPrefab works when passing the container entity of a nested prefab.
{
@ -162,7 +158,7 @@ namespace UnitTest
}
}
TEST_F(PrefabFocusTests, PrefabFocus_FocusOnOwningPrefab_NestedEntity)
TEST_F(PrefabFocusTests, FocusOnOwningPrefabNestedEntity)
{
// Verify FocusOnOwningPrefab works when passing a nested entity of the a nested prefab.
{
@ -176,7 +172,7 @@ namespace UnitTest
}
}
TEST_F(PrefabFocusTests, PrefabFocus_FocusOnOwningPrefab_Clear)
TEST_F(PrefabFocusTests, FocusOnOwningPrefabClear)
{
// Verify FocusOnOwningPrefab points to the root prefab when the focus is cleared.
{
@ -196,7 +192,32 @@ namespace UnitTest
}
}
TEST_F(PrefabFocusTests, PrefabFocus_IsOwningPrefabBeingFocused_Content)
TEST_F(PrefabFocusTests, FocusOnParentOfFocusedPrefabLeaf)
{
// Call FocusOnParentOfFocusedPrefab on a leaf instance and verify the parent is focused correctly.
{
m_prefabFocusPublicInterface->FocusOnOwningPrefab(m_instanceMap[CarEntityName]->GetContainerEntityId());
m_prefabFocusPublicInterface->FocusOnParentOfFocusedPrefab(m_editorEntityContextId);
EXPECT_EQ(
&m_prefabFocusInterface->GetFocusedPrefabInstance(m_editorEntityContextId)->get(),
m_instanceMap[StreetEntityName]
);
}
}
TEST_F(PrefabFocusTests, FocusOnParentOfFocusedPrefabRoot)
{
// Call FocusOnParentOfFocusedPrefab on the root instance and verify the operation fails.
{
m_prefabFocusPublicInterface->FocusOnOwningPrefab(m_instanceMap[CityEntityName]->GetContainerEntityId());
auto outcome = m_prefabFocusPublicInterface->FocusOnParentOfFocusedPrefab(m_editorEntityContextId);
EXPECT_FALSE(outcome.IsSuccess());
}
}
TEST_F(PrefabFocusTests, IsOwningPrefabBeingFocusedContent)
{
// Verify IsOwningPrefabBeingFocused returns true for all entities in a focused prefab (container/nested)
{
@ -207,7 +228,7 @@ namespace UnitTest
}
}
TEST_F(PrefabFocusTests, PrefabFocus_IsOwningPrefabBeingFocused_AncestorsDescendants)
TEST_F(PrefabFocusTests, IsOwningPrefabBeingFocusedAncestorsDescendants)
{
// Verify IsOwningPrefabBeingFocused returns false for all entities not in a focused prefab (ancestors/descendants)
{
@ -221,7 +242,7 @@ namespace UnitTest
}
}
TEST_F(PrefabFocusTests, PrefabFocus_IsOwningPrefabBeingFocused_Siblings)
TEST_F(PrefabFocusTests, IsOwningPrefabBeingFocusedSiblings)
{
// Verify IsOwningPrefabBeingFocused returns false for all entities not in a focused prefab (siblings)
{

@ -2942,11 +2942,20 @@ namespace AssetProcessor
// File is a source file that has been processed before
AZStd::string fingerprintFromDatabase = sourceFileItr->m_analysisFingerprint.toUtf8().data();
AZStd::string_view builderEntries(fingerprintFromDatabase.begin() + s_lengthOfUuid + 1, fingerprintFromDatabase.end());
AZStd::string_view dependencyFingerprint(fingerprintFromDatabase.begin(), fingerprintFromDatabase.begin() + s_lengthOfUuid);
int numBuildersEmittingSourceDependencies = 0;
if (!fingerprintFromDatabase.empty() && AreBuildersUnchanged(builderEntries, numBuildersEmittingSourceDependencies))
{
// Builder(s) have not changed since last time
AZStd::string currentFingerprint = ComputeRecursiveDependenciesFingerprint(
fileInfo.m_filePath.toUtf8().constData(), sourceFileItr->m_sourceDatabaseName.toUtf8().constData());
if(dependencyFingerprint != currentFingerprint)
{
// Dependencies have changed
return false;
}
// Success - we can skip this file, nothing has changed!
// Remove it from the list of to-be-processed files, otherwise the AP will assume the file was deleted
@ -4311,6 +4320,32 @@ namespace AssetProcessor
}
}
AZStd::string AssetProcessorManager::ComputeRecursiveDependenciesFingerprint(const AZStd::string& fileAbsolutePath, const AZStd::string& fileDatabaseName)
{
AZStd::string concatenatedFingerprints;
// QSet is not ordered.
SourceFilesForFingerprintingContainer knownDependenciesAbsolutePaths;
// this automatically adds the input file to the list:
QueryAbsolutePathDependenciesRecursive(QString::fromUtf8(fileDatabaseName.c_str()), knownDependenciesAbsolutePaths,
AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any, false);
AddMetadataFilesForFingerprinting(QString::fromUtf8(fileAbsolutePath.c_str()), knownDependenciesAbsolutePaths);
// reserve 17 chars for each since its a 64 bit hex number, and then one more for the dash inbetween each.
constexpr int bytesPerFingerprint = (sizeof(AZ::u64) * 2) + 1; // 2 HEX characters per byte +1 for the `-` we will add between each fingerprint
concatenatedFingerprints.reserve((knownDependenciesAbsolutePaths.size() * bytesPerFingerprint));
for (const auto& element : knownDependenciesAbsolutePaths)
{
// if its a placeholder then don't bother hitting the disk to find it.
concatenatedFingerprints.append(AssetUtilities::GetFileFingerprint(element.first, element.second));
concatenatedFingerprints.append("-");
}
// to keep this from growing out of hand, we don't use the full string, we use a hash of it:
return AZ::Uuid::CreateName(concatenatedFingerprints.c_str()).ToString<AZStd::string>();
}
void AssetProcessorManager::FinishAnalysis(AZStd::string fileToCheck)
{
using namespace AzToolsFramework::AssetDatabase;
@ -4378,29 +4413,9 @@ namespace AssetProcessor
if (found)
{
// construct the analysis fingerprint
// the format for this data is "modtimefingerprint:builder0:builder1:builder2:...:buildern"
source.m_analysisFingerprint.clear();
// compute mod times:
// get the appropriate modtimes:
AZStd::string modTimeArray;
// the format for this data is "hashfingerprint:builder0:builder1:builder2:...:buildern"
source.m_analysisFingerprint = ComputeRecursiveDependenciesFingerprint(fileToCheck, analysisTracker->m_databaseSourceName);
// QSet is not ordered.
SourceFilesForFingerprintingContainer knownDependenciesAbsolutePaths;
// this automatically adds the input file to the list:
QueryAbsolutePathDependenciesRecursive(QString::fromUtf8(analysisTracker->m_databaseSourceName.c_str()), knownDependenciesAbsolutePaths, SourceFileDependencyEntry::DEP_Any, false);
AddMetadataFilesForFingerprinting(QString::fromUtf8(fileToCheck.c_str()), knownDependenciesAbsolutePaths);
// reserve 17 chars for each since its a 64 bit hex number, and then one more for the dash inbetween each.
modTimeArray.reserve((knownDependenciesAbsolutePaths.size() * 17));
for (const auto& element : knownDependenciesAbsolutePaths)
{
// if its a placeholder then don't bother hitting the disk to find it.
modTimeArray.append(AssetUtilities::GetFileFingerprint(element.first, element.second));
modTimeArray.append("-");
}
// to keep this from growing out of hand, we don't use the full string, we use a hash of it:
source.m_analysisFingerprint = AZ::Uuid::CreateName(modTimeArray.c_str()).ToString<AZStd::string>();
for (const AZ::Uuid& builderID : analysisTracker->m_buildersInvolved)
{
source.m_analysisFingerprint.append(":");
@ -4423,7 +4438,7 @@ namespace AssetProcessor
const ScanFolderInfo* scanFolder = m_platformConfig->GetScanFolderForFile(fileToCheck.c_str());
scanFolderPk = aznumeric_cast<int>(scanFolder->ScanFolderID());
m_platformConfig->ConvertToRelativePath(fileToCheck.c_str(), scanFolder, databaseSourceName);
PlatformConfiguration::ConvertToRelativePath(fileToCheck.c_str(), scanFolder, databaseSourceName);
}
// Record the modtime for the file so we know we processed it

@ -485,6 +485,7 @@ namespace AssetProcessor
};
void ComputeBuilderDirty();
AZStd::string ComputeRecursiveDependenciesFingerprint(const AZStd::string& fileAbsolutePath, const AZStd::string& fileDatabaseName);
AZStd::unordered_map<AZ::Uuid, BuilderData> m_builderDataCache;
bool m_buildersAddedOrRemoved = true; //< true if any new builders exist. If this happens we actually need to re-analyze everything.
bool m_anyBuilderChange = true;

@ -516,6 +516,79 @@ namespace UnitTests
ASSERT_EQ(m_data->m_processResults.size(), 2);
}
TEST_F(ModtimeScanningTest, AssetProcessorIsRestartedBeforeDependencyIsProcessed_DependencyIsProcessedOnStart)
{
using namespace AzToolsFramework::AssetSystem;
auto theFile = m_data->m_absolutePath[1].toUtf8();
const char* theFileString = theFile.constData();
SetFileContents(theFileString, "hello world");
// Enable the features we're testing
m_assetProcessorManager->SetEnableModtimeSkippingFeature(true);
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers
// the other test file to process as well
ExpectWork(2, 2);
// Sort the results and process the first one, which should always be the modtimeTestDependency.txt file
// which is the same file we modified above. modtimeTestFile.txt depends on this file but we're not going to process it yet.
{
std::sort(
m_data->m_processResults.begin(), m_data->m_processResults.end(),
[](decltype(m_data->m_processResults[0])& left, decltype(left)& right)
{
return left.m_jobEntry.m_databaseSourceName < right.m_jobEntry.m_databaseSourceName;
});
const auto& processResult = m_data->m_processResults[0];
auto file =
QDir(processResult.m_destinationPath).absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName.toLower() + ".arc1");
m_data->m_productPaths.emplace(
QDir(processResult.m_jobEntry.m_watchFolderPath)
.absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName)
.toUtf8()
.constData(),
file);
// Create the file on disk
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(file, "products."));
AssetBuilderSDK::ProcessJobResponse response;
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct(file.toUtf8().constData(), AZ::Uuid::CreateNull(), 1));
using JobEntry = AssetProcessor::JobEntry;
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "AssetProcessed", Qt::QueuedConnection, Q_ARG(JobEntry, processResult.m_jobEntry),
Q_ARG(AssetBuilderSDK::ProcessJobResponse, response));
}
ASSERT_TRUE(BlockUntilIdle(5000));
// Shutdown and restart the APM
m_assetProcessorManager.reset();
m_assetProcessorManager = AZStd::make_unique<AssetProcessorManager_Test>(m_config.get());
SetUpAssetProcessorManager();
m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
m_data->m_processResults.clear();
m_data->m_deletedSources.clear();
// Re-run the scanner on our files
filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Expect processing to resume on the job we didn't process before
ExpectWork(1, 1);
}
void DeleteTest::SetUp()
{
AssetProcessorManagerTest::SetUp();

@ -0,0 +1,56 @@
{
"Type": "JsonSerialization",
"Version": 1,
"ClassName": "MultiplatformPresetSettings",
"ClassData": {
"DefaultPreset": {
"UUID": "{C5E76E09-39FA-411F-B2E2-15B47BB6AB5F}",
"Name": "GSI",
"OutputTypeHandling": "UseInputFormat",
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"PlatformsPresets": {
"android": {
"UUID": "{C5E76E09-39FA-411F-B2E2-15B47BB6AB5F}",
"Name": "GSI",
"OutputTypeHandling": "UseInputFormat",
"MaxTextureSize": 2048,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"ios": {
"UUID": "{C5E76E09-39FA-411F-B2E2-15B47BB6AB5F}",
"Name": "GSI",
"OutputTypeHandling": "UseInputFormat",
"MaxTextureSize": 2048,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"mac": {
"UUID": "{C5E76E09-39FA-411F-B2E2-15B47BB6AB5F}",
"Name": "GSI",
"OutputTypeHandling": "UseInputFormat",
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"provo": {
"UUID": "{C5E76E09-39FA-411F-B2E2-15B47BB6AB5F}",
"Name": "GSI",
"OutputTypeHandling": "UseInputFormat",
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
}
}
}
}

@ -0,0 +1,61 @@
{
"Type": "JsonSerialization",
"Version": 1,
"ClassName": "MultiplatformPresetSettings",
"ClassData": {
"DefaultPreset": {
"UUID": "{181FE328-5408-4722-895F-1BB61803997B}",
"Name": "GSI16",
"PixelFormat": "R16",
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"PlatformsPresets": {
"android": {
"UUID": "{181FE328-5408-4722-895F-1BB61803997B}",
"Name": "GSI16",
"PixelFormat": "R16",
"MaxTextureSize": 2048,
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"ios": {
"UUID": "{181FE328-5408-4722-895F-1BB61803997B}",
"Name": "GSI16",
"PixelFormat": "R16",
"MaxTextureSize": 2048,
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"mac": {
"UUID": "{181FE328-5408-4722-895F-1BB61803997B}",
"Name": "GSI16",
"PixelFormat": "R16",
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"provo": {
"UUID": "{181FE328-5408-4722-895F-1BB61803997B}",
"Name": "GSI16",
"PixelFormat": "R16",
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
}
}
}
}

@ -0,0 +1,61 @@
{
"Type": "JsonSerialization",
"Version": 1,
"ClassName": "MultiplatformPresetSettings",
"ClassData": {
"DefaultPreset": {
"UUID": "{604FB174-7165-4F6E-889A-3B91DEC9311C}",
"Name": "GSI32",
"PixelFormat": "R32",
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"PlatformsPresets": {
"android": {
"UUID": "{604FB174-7165-4F6E-889A-3B91DEC9311C}",
"Name": "GSI32",
"PixelFormat": "R32",
"MaxTextureSize": 2048,
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"ios": {
"UUID": "{604FB174-7165-4F6E-889A-3B91DEC9311C}",
"Name": "GSI32",
"PixelFormat": "R32",
"MaxTextureSize": 2048,
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"mac": {
"UUID": "{604FB174-7165-4F6E-889A-3B91DEC9311C}",
"Name": "GSI32",
"PixelFormat": "R32",
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"provo": {
"UUID": "{604FB174-7165-4F6E-889A-3B91DEC9311C}",
"Name": "GSI32",
"PixelFormat": "R32",
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
}
}
}
}

@ -0,0 +1,61 @@
{
"Type": "JsonSerialization",
"Version": 1,
"ClassName": "MultiplatformPresetSettings",
"ClassData": {
"DefaultPreset": {
"UUID": "{84B1FE72-AD1A-4E50-83CC-4253ABA59733}",
"Name": "GSI8",
"PixelFormat": "R8",
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"PlatformsPresets": {
"android": {
"UUID": "{84B1FE72-AD1A-4E50-83CC-4253ABA59733}",
"Name": "GSI8",
"PixelFormat": "R8",
"MaxTextureSize": 2048,
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"ios": {
"UUID": "{84B1FE72-AD1A-4E50-83CC-4253ABA59733}",
"Name": "GSI8",
"PixelFormat": "R8",
"MaxTextureSize": 2048,
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"mac": {
"UUID": "{84B1FE72-AD1A-4E50-83CC-4253ABA59733}",
"Name": "GSI8",
"PixelFormat": "R8",
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
},
"provo": {
"UUID": "{84B1FE72-AD1A-4E50-83CC-4253ABA59733}",
"Name": "GSI8",
"PixelFormat": "R8",
"DiscardAlpha": true,
"IsPowerOf2": true,
"MipMapSetting": {
"MipGenType": "Box"
}
}
}
}
}

@ -146,7 +146,11 @@
// decal
"_decal": [ "Decal_AlbedoWithOpacity" ],
// ui
"_ui": [ "UserInterface_Compressed","UserInterface_Lossless" ]
"_ui": [ "UserInterface_Compressed","UserInterface_Lossless" ],
"_gsi": [ "GSI" ],
"_gsi8": [ "GSI8" ],
"_gsi16": [ "GSI16" ],
"_gsi32": [ "GSI32" ]
},
"DefaultPreset": "Albedo",
"DefaultPresetAlpha": "AlbedoWithGenericAlpha"

@ -50,7 +50,9 @@ namespace ImageProcessingAtom
->Field("NumberResidentMips", &PresetSettings::m_numResidentMips)
->Field("Swizzle", &PresetSettings::m_swizzle)
->Field("CubemapSettings", &PresetSettings::m_cubemapSetting)
->Field("MipMapSetting", &PresetSettings::m_mipmapSetting);
->Field("MipMapSetting", &PresetSettings::m_mipmapSetting)
->Field("OutputTypeHandling", &PresetSettings::m_outputTypeHandling)
;
serialize->Enum<RGBWeight>()
->Value("Uniform", RGBWeight::uniform)
@ -130,6 +132,11 @@ namespace ImageProcessingAtom
->Value("R32", EPixelFormat::ePixelFormat_R32)
->Value("Unknown", EPixelFormat::ePixelFormat_Unknown)
;
serialize->Enum<OutputTypeHandling>()
->Value("Default", OutputTypeHandling::UseSpecifiedOutputType)
->Value("UseInputFormat", OutputTypeHandling::UseInputFormat)
;
}
}
@ -200,7 +207,9 @@ namespace ImageProcessingAtom
m_glossFromNormals == other.m_glossFromNormals &&
m_swizzle == other.m_swizzle &&
m_isMipRenormalize == other.m_isMipRenormalize &&
m_numResidentMips == other.m_numResidentMips;
m_numResidentMips == other.m_numResidentMips &&
m_outputTypeHandling == other.m_outputTypeHandling
;
}
void PresetSettings::DeepCopyMembers(const PresetSettings& other)
@ -237,6 +246,7 @@ namespace ImageProcessingAtom
m_swizzle = other.m_swizzle;
m_isMipRenormalize = other.m_isMipRenormalize;
m_numResidentMips = other.m_numResidentMips;
m_outputTypeHandling = other.m_outputTypeHandling;
}
}

@ -25,6 +25,13 @@ namespace ImageProcessingAtom
AZ_TYPE_INFO(PresetSettings, "{4F4DEC5C-48DD-40FD-97B4-5FB6FC7242E9}");
AZ_CLASS_ALLOCATOR(PresetSettings, AZ::SystemAllocator, 0);
//! Custom overrides for how to handle the output format
enum OutputTypeHandling
{
UseSpecifiedOutputType = 0,
UseInputFormat
};
PresetSettings();
PresetSettings(const PresetSettings& other);
PresetSettings& operator= (const PresetSettings& other);
@ -103,6 +110,9 @@ namespace ImageProcessingAtom
//"swizzle". need to be 4 character and each character need to be one of "rgba01"
AZStd::string m_swizzle;
//! Controls how the output type format is derived
OutputTypeHandling m_outputTypeHandling = UseSpecifiedOutputType;
protected:
void DeepCopyMembers(const PresetSettings& other);
};
@ -136,3 +146,10 @@ namespace ImageProcessingAtom
};
} // namespace ImageProcessingAtom
namespace AZ
{
// Bind enums with uuids. Required for named enum support.
// Note: AZ_TYPE_INFO_SPECIALIZE has to be declared in AZ namespace
AZ_TYPE_INFO_SPECIALIZE(ImageProcessingAtom::PresetSettings::OutputTypeHandling, "{F919ECB6-BF80-4BEF-9E72-EA76504EBE9D}");
}

@ -535,7 +535,21 @@ namespace ImageProcessingAtom
m_image->GetCompressOption().rgbWeight = m_input->m_presetSetting.GetColorWeight();
m_image->GetCompressOption().discardAlpha = m_input->m_presetSetting.m_discardAlpha;
m_image->ConvertFormat(m_input->m_presetSetting.m_pixelFormat);
// Convert to a pixel format based on the desired handling
// The default behavior will choose the output format specified by the preset
EPixelFormat outputFormat;
switch (m_input->m_presetSetting.m_outputTypeHandling)
{
case PresetSettings::OutputTypeHandling::UseInputFormat:
outputFormat = m_input->m_inputImage->GetPixelFormat();
break;
case PresetSettings::OutputTypeHandling::UseSpecifiedOutputType:
default:
outputFormat = m_input->m_presetSetting.m_pixelFormat;
break;
}
m_image->ConvertFormat(outputFormat);
return true;
}

@ -191,7 +191,11 @@ namespace EMotionFX::MotionMatching
// set the current time to the new calculated time
uniqueData->ClearInheritFlags();
uniqueData->SetPreSyncTime(instance->GetMotionInstance()->GetCurrentTime());
if (instance->GetMotionInstance())
{
uniqueData->SetPreSyncTime(instance->GetMotionInstance()->GetCurrentTime());
}
uniqueData->SetCurrentPlayTime(instance->GetNewMotionTime());
if (uniqueData->GetPreSyncTime() > uniqueData->GetCurrentPlayTime())
@ -230,9 +234,12 @@ namespace EMotionFX::MotionMatching
}
MotionInstance* motionInstance = instance->GetMotionInstance();
motionInstance->UpdateByTimeValues(uniqueData->GetPreSyncTime(), uniqueData->GetCurrentPlayTime(), &data->GetEventBuffer());
if (motionInstance)
{
motionInstance->UpdateByTimeValues(uniqueData->GetPreSyncTime(), uniqueData->GetCurrentPlayTime(), &data->GetEventBuffer());
uniqueData->SetCurrentPlayTime(motionInstance->GetCurrentTime());
}
uniqueData->SetCurrentPlayTime(motionInstance->GetCurrentTime());
data->GetEventBuffer().UpdateEmitters(this);
instance->PostUpdate(timePassedInSeconds);
@ -240,7 +247,6 @@ namespace EMotionFX::MotionMatching
const Transform& trajectoryDelta = instance->GetMotionExtractionDelta();
data->SetTrajectoryDelta(trajectoryDelta);
data->SetTrajectoryDeltaMirrored(trajectoryDelta); // TODO: use a real mirrored version here.
m_postUpdateTimeInMs = m_timer.GetDeltaTimeInSeconds() * 1000.0f;
}
@ -251,12 +257,10 @@ namespace EMotionFX::MotionMatching
AZ_UNUSED(animGraphInstance);
m_timer.Stamp();
AnimGraphPose* outputPose;
// Initialize to bind pose.
ActorInstance* actorInstance = animGraphInstance->GetActorInstance();
RequestPoses(animGraphInstance);
outputPose = GetOutputPose(animGraphInstance, OUTPUTPORT_POSE)->GetValue();
AnimGraphPose* outputPose = GetOutputPose(animGraphInstance, OUTPUTPORT_POSE)->GetValue();
outputPose->InitFromBindPose(actorInstance);
if (m_disabled)
@ -287,7 +291,6 @@ namespace EMotionFX::MotionMatching
// Performance metrics
m_outputTimeInMs = m_timer.GetDeltaTimeInSeconds() * 1000.0f;
{
//AZ_Printf("MotionMatch", "Update = %.2f, PostUpdate = %.2f, Output = %.2f", m_updateTime, m_postUpdateTime, m_outputTime);
#ifdef IMGUI_ENABLED
ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::PushPerformanceHistogramValue, "Update", m_updateTimeInMs);
ImGuiMonitorRequestBus::Broadcast(&ImGuiMonitorRequests::PushPerformanceHistogramValue, "Post Update", m_postUpdateTimeInMs);
@ -298,6 +301,16 @@ namespace EMotionFX::MotionMatching
instance->DebugDraw();
}
AZ::Crc32 BlendTreeMotionMatchNode::GetTrajectoryPathSettingsVisibility() const
{
if (m_trajectoryQueryMode == TrajectoryQuery::MODE_TARGETDRIVEN)
{
return AZ::Edit::PropertyVisibility::Hide;
}
return AZ::Edit::PropertyVisibility::Show;
}
void BlendTreeMotionMatchNode::Reflect(AZ::ReflectContext* context)
{
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
@ -339,32 +352,32 @@ namespace EMotionFX::MotionMatching
->Attribute(AZ::Edit::Attributes::Max, std::numeric_limits<float>::max())
->Attribute(AZ::Edit::Attributes::Step, 0.05f)
->DataElement(AZ::Edit::UIHandlers::Default, &BlendTreeMotionMatchNode::m_maxKdTreeDepth, "Max kdTree depth", "The maximum number of hierarchy levels in the kdTree.")
->Attribute(AZ::Edit::Attributes::Min, 1)
->Attribute(AZ::Edit::Attributes::Max, 20)
->Attribute(AZ::Edit::Attributes::ChangeNotify, &BlendTreeMotionMatchNode::Reinit)
->Attribute(AZ::Edit::Attributes::Min, 1)
->Attribute(AZ::Edit::Attributes::Max, 20)
->Attribute(AZ::Edit::Attributes::ChangeNotify, &BlendTreeMotionMatchNode::Reinit)
->DataElement(AZ::Edit::UIHandlers::Default, &BlendTreeMotionMatchNode::m_minFramesPerKdTreeNode, "Min kdTree node size", "The minimum number of frames to store per kdTree node.")
->Attribute(AZ::Edit::Attributes::Min, 1)
->Attribute(AZ::Edit::Attributes::Max, 100000)
->Attribute(AZ::Edit::Attributes::ChangeNotify, &BlendTreeMotionMatchNode::Reinit)
->Attribute(AZ::Edit::Attributes::Min, 1)
->Attribute(AZ::Edit::Attributes::Max, 100000)
->Attribute(AZ::Edit::Attributes::ChangeNotify, &BlendTreeMotionMatchNode::Reinit)
->DataElement(AZ::Edit::UIHandlers::ComboBox, &BlendTreeMotionMatchNode::m_trajectoryQueryMode, "Trajectory Prediction", "Desired future trajectory generation mode.")
->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree)
->EnumAttribute(TrajectoryQuery::MODE_TARGETDRIVEN, "Target-driven")
->EnumAttribute(TrajectoryQuery::MODE_AUTOMATIC, "Automatic (Demo)")
->DataElement(AZ::Edit::UIHandlers::Default, &BlendTreeMotionMatchNode::m_pathRadius, "Path radius", "")
->Attribute(AZ::Edit::Attributes::Min, 0.0001f)
->Attribute(AZ::Edit::Attributes::Max, std::numeric_limits<float>::max())
->Attribute(AZ::Edit::Attributes::Step, 0.01f)
->Attribute(AZ::Edit::Attributes::Visibility, &BlendTreeMotionMatchNode::GetTrajectoryPathSettingsVisibility)
->Attribute(AZ::Edit::Attributes::Min, 0.0001f)
->Attribute(AZ::Edit::Attributes::Max, std::numeric_limits<float>::max())
->Attribute(AZ::Edit::Attributes::Step, 0.01f)
->DataElement(AZ::Edit::UIHandlers::Default, &BlendTreeMotionMatchNode::m_pathSpeed, "Path speed", "")
->Attribute(AZ::Edit::Attributes::Min, 0.0001f)
->Attribute(AZ::Edit::Attributes::Max, std::numeric_limits<float>::max())
->Attribute(AZ::Edit::Attributes::Step, 0.01f)
->DataElement(AZ::Edit::UIHandlers::ComboBox, &BlendTreeMotionMatchNode::m_trajectoryQueryMode, "Trajectory mode", "Desired future trajectory generation mode.")
->EnumAttribute(TrajectoryQuery::MODE_TARGETDRIVEN, "Target driven")
->EnumAttribute(TrajectoryQuery::MODE_ONE, "Mode one")
->EnumAttribute(TrajectoryQuery::MODE_TWO, "Mode two")
->EnumAttribute(TrajectoryQuery::MODE_THREE, "Mode three")
->EnumAttribute(TrajectoryQuery::MODE_FOUR, "Mode four")
->Attribute(AZ::Edit::Attributes::Visibility, &BlendTreeMotionMatchNode::GetTrajectoryPathSettingsVisibility)
->Attribute(AZ::Edit::Attributes::Min, 0.0001f)
->Attribute(AZ::Edit::Attributes::Max, std::numeric_limits<float>::max())
->Attribute(AZ::Edit::Attributes::Step, 0.01f)
->DataElement(AZ::Edit::UIHandlers::Default, &BlendTreeMotionMatchNode::m_featureSchema, "FeatureSchema", "")
->Attribute(AZ::Edit::Attributes::AutoExpand, "")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
->Attribute(AZ::Edit::Attributes::ChangeNotify, &BlendTreeMotionMatchNode::Reinit)
->DataElement(AZ_CRC("MotionSetMotionIds", 0x8695c0fa), &BlendTreeMotionMatchNode::m_motionIds, "Motions", "")
->DataElement(AZ_CRC_CE("MotionSetMotionIds"), &BlendTreeMotionMatchNode::m_motionIds, "Motions", "")
->Attribute(AZ::Edit::Attributes::ChangeNotify, &BlendTreeMotionMatchNode::Reinit)
->Attribute(AZ::Edit::Attributes::ContainerCanBeModified, false)
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::HideChildren)

@ -87,6 +87,8 @@ namespace EMotionFX::MotionMatching
void Update(AnimGraphInstance* animGraphInstance, float timePassedInSeconds) override;
void PostUpdate(AnimGraphInstance* animGraphInstance, float timePassedInSeconds) override;
AZ::Crc32 GetTrajectoryPathSettingsVisibility() const;
FeatureSchema m_featureSchema;
AZStd::vector<AZStd::string> m_motionIds;

@ -35,6 +35,15 @@ namespace EMotionFX::MotionMatching
class MotionMatchingInstance;
class TrajectoryQuery;
//! A feature is a property extracted from the animation data and is used by the motion matching algorithm to find the next best matching frame.
//! Examples of features are the position of the feet joints, the linear or angular velocity of the knee joints or the trajectory history and future
//! trajectory of the root joint. We can also encode environment sensations like obstacle positions and height, the location of the sword of an enemy
//! character or a football's position and velocity. Their purpose is to describe a frame of the animation by their key characteristics and sometimes
//! enhance the actual keyframe data (pos/rot/scale per joint) by e.g. taking the time domain into account and calculate the velocity or acceleration,
//! or a whole trajectory to describe where the given joint came from to reach the frame and the path it moves along in the near future.
//! @Note: Features are extracted and stored relative to a given joint, in most cases the motion extraction or root joint, and thus are in model-space.
//! This makes the search algorithm invariant to the character location and orientation and the extracted features, like e.g. a joint position or velocity,
//! translate and rotate along with the character.
class EMFX_API Feature
{
public:
@ -138,16 +147,14 @@ namespace EMotionFX::MotionMatching
static void CalculateVelocity(const ActorInstance* actorInstance, size_t jointIndex, size_t relativeToJointIndex, const Frame& frame, AZ::Vector3& outVelocity);
protected:
/**
* Calculate a normalized direction vector difference between the two given vectors.
* A dot product of the two vectors is taken and the result in range [-1, 1] is scaled to [0, 1].
* @result Normalized, absolute difference between the vectors.
* Angle difference dot result cost
* 0.0 degrees 1.0 0.0
* 90.0 degrees 0.0 0.5
* 180.0 degrees -1.0 1.0
* 270.0 degrees 0.0 0.5
**/
//! Calculate a normalized direction vector difference between the two given vectors.
//! A dot product of the two vectors is taken and the result in range [-1, 1] is scaled to [0, 1].
//! @result Normalized, absolute difference between the vectors.
//! Angle difference dot result cost
//! 0.0 degrees 1.0 0.0
//! 90.0 degrees 0.0 0.5
//! 180.0 degrees -1.0 1.0
//! 270.0 degrees 0.0 0.5
float GetNormalizedDirectionDifference(const AZ::Vector2& directionA, const AZ::Vector2& directionB) const;
float GetNormalizedDirectionDifference(const AZ::Vector3& directionA, const AZ::Vector3& directionB) const;
@ -164,7 +171,7 @@ namespace EMotionFX::MotionMatching
AZ::Color m_debugColor = AZ::Colors::Green; //< Color used for debug visualizations to identify the feature.
bool m_debugDrawEnabled = false; //< Are debug visualizations enabled for this feature?
float m_costFactor = 1.0f; //< The cost factor for the feature is multiplied with the actual and can be used to change a feature's influence in the motion matching search.
ResidualType m_residualType = ResidualType::Squared; //< How do we calculate the differences (residuals) between the input query values and the frames in the motion database that sum up the feature cost.
ResidualType m_residualType = ResidualType::Absolute; //< How do we calculate the differences (residuals) between the input query values and the frames in the motion database that sum up the feature cost.
// Instance data (depends on the feature schema or actor instance).
FeatureMatrix::Index m_featureColumnOffset; //< Float/Value offset, starting column for where the feature should be places at.

@ -15,6 +15,8 @@
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/string/string.h>
//! Enable in case you want to use the Eigen SDK Eigen::Matrix as base for the feature matrix (https://eigen.tuxfamily.org/)
//! In case Eigen is disabled, a small simple NxM wrapper class is provided by default.
//#define O3DE_USE_EIGEN
#define O3DE_MM_FLOATTYPE float
@ -37,9 +39,7 @@ namespace EMotionFX::MotionMatching
// RowMajor: Store row components next to each other in memory for cache-optimized feature access for a given frame.
using FeatureMatrixType = Eigen::Matrix<O3DE_MM_FLOATTYPE, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>;
#else
/**
* Small wrapper for a 2D matrix similar to the Eigen::Matrix.
*/
//! Small wrapper for a 2D matrix similar to the Eigen::Matrix.
class FeatureMatrixType
{
public:
@ -87,6 +87,12 @@ namespace EMotionFX::MotionMatching
};
#endif
//! The feature matrix is a NxM matrix which stores the extracted feature values for all frames in our motion database based upon a given feature schema.
//! The feature schema defines the order of the columns and values and is used to identify values and find their location inside the matrix.
//! A 3D position feature storing XYZ values e.g. will use three columns in the feature matrix. Every component of a feature is linked to a column index,
//! so e.g. the left foot position Y value might be at column index 6. The group of values or columns that belong to a given feature is what we call a feature block.
//! The accumulated number of dimensions for all features in the schema, while the number of dimensions might vary per feature, form the number of columns of the feature matrix.
//! Each row represents the features of a single frame of the motion database. The number of rows of the feature matrix is defined by the number.
class FeatureMatrix
: public FeatureMatrixType
{

@ -29,12 +29,10 @@ namespace EMotionFX::MotionMatching
{
class FrameDatabase;
/**
* Matches the root joint past and future trajectory.
* For each frame in the motion database, the position and facing direction relative to the current frame of the joint will be evaluated for a past and future time window.
* The past and future samples together form the trajectory of the current frame within the time window. This basically describes where the character came from to reach the
* current frame and where it will go when continuing to play the animation.
**/
//! Matches the root joint past and future trajectory.
//! For each frame in the motion database, the position and facing direction relative to the current frame of the joint will be evaluated for a past and future time window.
//! The past and future samples together form the trajectory of the current frame within the time window. This basically describes where the character came from to reach the
//! current frame and where it will go when continuing to play the animation.
class EMFX_API FeatureTrajectory
: public Feature
{

@ -20,10 +20,8 @@ namespace EMotionFX
namespace MotionMatching
{
/**
* A motion matching frame.
* This holds information required in order to extract a given pose in a given motion.
*/
//! A motion matching frame.
//! This holds information required in order to extract a given pose in a given motion.
class EMFX_API Frame
{
public:
@ -53,10 +51,10 @@ namespace EMotionFX
void SetMirrored(bool enabled);
private:
size_t m_frameIndex = 0; /**< The motion frame index inside the data object. */
float m_sampleTime = 0.0f; /**< The time offset in the original motion. */
Motion* m_sourceMotion = nullptr; /**< The original motion that we sample from to restore the pose. */
bool m_mirrored = false; /**< Is this frame mirrored? */
size_t m_frameIndex = 0; //< The motion frame index inside the data object.
float m_sampleTime = 0.0f; //< The time offset in the original motion.
Motion* m_sourceMotion = nullptr; //< The original motion that we sample from to restore the pose.
bool m_mirrored = false; //< Is this frame mirrored?
};
} // namespace MotionMatching
} // namespace EMotionFX

@ -53,7 +53,7 @@ namespace EMotionFX::MotionMatching
m_usedMotions.shrink_to_fit();
}
void FrameDatabase::ExtractActiveMotionEventDatas(const Motion* motion, float time, AZStd::vector<EventData*>& activeEventDatas)
void FrameDatabase::ExtractActiveMotionEventDatas(const Motion* motion, float time, AZStd::vector<EventData*>& activeEventDatas) const
{
activeEventDatas.clear();
@ -84,9 +84,11 @@ namespace EMotionFX::MotionMatching
}
}
bool FrameDatabase::IsFrameDiscarded(const AZStd::vector<EventData*>& activeEventDatas) const
bool FrameDatabase::IsFrameDiscarded(const Motion* motion, float frameTime, AZStd::vector<EventData*>& activeEvents) const
{
for (const EventData* eventData : activeEventDatas)
// Is frame discarded by a motion event?
ExtractActiveMotionEventDatas(motion, frameTime, activeEvents);
for (const EventData* eventData : activeEvents)
{
if (eventData->RTTI_GetType() == azrtti_typeid<DiscardFrameEventData>())
{
@ -126,11 +128,10 @@ namespace EMotionFX::MotionMatching
double curTime = 0.0;
while (curTime <= totalTime)
{
const float floatTime = aznumeric_cast<float>(curTime);
ExtractActiveMotionEventDatas(motion, floatTime, activeEvents);
if (!IsFrameDiscarded(activeEvents))
const float frameTime = aznumeric_cast<float>(curTime);
if (!IsFrameDiscarded(motion, frameTime, activeEvents))
{
ImportFrame(motion, floatTime, mirrored);
ImportFrame(motion, frameTime, mirrored);
numFramesImported++;
}
else
@ -143,11 +144,10 @@ namespace EMotionFX::MotionMatching
// Make sure we include the last frame, if we stepped over it.
if (curTime - timeStep < totalTime - 0.000001)
{
const float floatTime = aznumeric_cast<float>(totalTime);
ExtractActiveMotionEventDatas(motion, floatTime, activeEvents);
if (!IsFrameDiscarded(activeEvents))
const float frameTime = aznumeric_cast<float>(totalTime);
if (!IsFrameDiscarded(motion, frameTime, activeEvents))
{
ImportFrame(motion, floatTime, mirrored);
ImportFrame(motion, frameTime, mirrored);
numFramesImported++;
}
else

@ -29,29 +29,36 @@ namespace EMotionFX::MotionMatching
class MotionMatchingInstance;
class MotionMatchEventData;
// The motion matching data.
// This is basically a database of frames (which point to motion objects), together with meta data per frame.
// No actual pose data is stored directly inside this class, just references to the right sample times inside specific motions.
//! A set of frames from your animations sampled at a given sample rate is stored in the frame database. A frame object knows about its index in the frame database,
//! the animation it belongs to and the sample time in seconds. It does not hold the actual sampled pose for memory reasons as the `EMotionFX::Motion` already store the
//! transform keyframes.
//! The sample rate of the animation might differ from the sample rate used for the frame database. For example, your animations might be recorded with 60 Hz while we only want
//! to extract the features with a sample rate of 30 Hz. As the motion matching algorithm is blending between the frames in the motion database while playing the animation window
//! between the jumps/blends, it can make sense to have animations with a higher sample rate than we use to extract the features.
//! A frame of the motion database can be used to sample a pose from which we can extract the features. It also provides functionality to sample a pose with a time offset to that frame.
//! This can be handy in order to calculate joint velocities or trajectory samples.
//! When importing animations, frames that are within the range of a discard frame motion event are ignored and won't be added to the motion database. Discard motion events can be
//! used to cut out sections of the imported animations that are unwanted like a stretching part between two dance cards.
class EMFX_API FrameDatabase
{
public:
AZ_RTTI(FrameDatabase, "{3E5ED4F9-8975-41F2-B665-0086368F0DDA}")
AZ_CLASS_ALLOCATOR_DECL
// The settings used when importing motions into the frame database.
// Used in combination with ImportFrames().
//! The settings used when importing motions into the frame database.
//! Used in combination with ImportFrames().
struct EMFX_API FrameImportSettings
{
size_t m_sampleRate = 30; /**< Sample at 30 frames per second on default. */
bool m_autoShrink = true; /**< Automatically shrink the internal frame arrays to their minimum size afterwards. */
size_t m_sampleRate = 30; //< Sample at 30 frames per second on default.
bool m_autoShrink = true; //< Automatically shrink the internal frame arrays to their minimum size afterwards.
};
FrameDatabase();
virtual ~FrameDatabase();
// Main functions.
AZStd::tuple<size_t, size_t> ImportFrames(Motion* motion, const FrameImportSettings& settings, bool mirrored); // Returns the number of imported frames and the number of discarded frames as second element.
void Clear(); // Clear the data, so you can re-initialize it with new data.
AZStd::tuple<size_t, size_t> ImportFrames(Motion* motion, const FrameImportSettings& settings, bool mirrored); //< Returns the number of imported frames and the number of discarded frames as second element.
void Clear(); //< Clear the data, so you can re-initialize it with new data.
// Statistics.
size_t GetNumFrames() const;
@ -66,21 +73,19 @@ namespace EMotionFX::MotionMatching
const AZStd::vector<const Motion*>& GetUsedMotions() const;
size_t GetSampleRate() const { return m_sampleRate; }
/**
* Find the frame index for the given playtime and motion.
* NOTE: This is a slow operation and should not be used by the runtime without visual debugging.
*/
//! Find the frame index for the given playtime and motion.
//! NOTE: This is a slow operation and should not be used by the runtime without visual debugging.
size_t FindFrameIndex(Motion* motion, float playtime) const;
private:
void ImportFrame(Motion* motion, float timeValue, bool mirrored);
bool IsFrameDiscarded(const AZStd::vector<EventData*>& activeEventDatas) const;
void ExtractActiveMotionEventDatas(const Motion* motion, float time, AZStd::vector<EventData*>& activeEventDatas); // Vector will be cleared internally.
bool IsFrameDiscarded(const Motion* motion, float frameTime, AZStd::vector<EventData*>& activeEvents) const;
void ExtractActiveMotionEventDatas(const Motion* motion, float time, AZStd::vector<EventData*>& activeEventDatas) const; // Vector will be cleared internally.
private:
AZStd::vector<Frame> m_frames; /**< The collection of frames. Keep in mind these don't hold a pose, but reference to a given frame/time value inside a given motion. */
AZStd::vector<Frame> m_frames; //< The collection of frames. Keep in mind these don't hold a pose, but reference to a given frame/time value inside a given motion.
AZStd::unordered_map<Motion*, AZStd::vector<size_t>> m_frameIndexByMotion;
AZStd::vector<const Motion*> m_usedMotions; /**< The list of used motions. */
AZStd::vector<const Motion*> m_usedMotions; //< The list of used motions.
size_t m_sampleRate = 0;
};
} // namespace EMotionFX::MotionMatching

@ -42,17 +42,21 @@ namespace EMotionFX::MotionMatching
size_t maxDepth,
size_t minFramesPerLeaf)
{
#if !defined(_RELEASE)
AZ::Debug::Timer timer;
timer.Stamp();
#endif
Clear();
// Verify the dimensions.
// Going above a 20 dimensional tree would start eating up too much memory.
m_numDimensions = CalcNumDimensions(features);
if (m_numDimensions == 0 || m_numDimensions > 20)
// Going above a 48 dimensional tree would start eating up too much memory.
const size_t maxNumDimensions = 48;
if (m_numDimensions == 0 || m_numDimensions > maxNumDimensions)
{
AZ_Error("Motion Matching", false, "Cannot initialize KD-tree. KD-tree dimension (%d) has to be between 1 and 20. Please use Feature::SetIncludeInKdTree(false) on some features.", m_numDimensions);
AZ_Error("Motion Matching", false, "Cannot initialize KD-tree. KD-tree dimension (%d) has to be between 1 and %zu. Please use Feature::SetIncludeInKdTree(false) on some features.", m_numDimensions, maxNumDimensions);
return false;
}
@ -78,6 +82,7 @@ namespace EMotionFX::MotionMatching
ClearFramesForNonEssentialNodes();
RemoveZeroFrameLeafNodes();
#if !defined(_RELEASE)
const float initTime = timer.GetDeltaTimeInSeconds();
AZ_TracePrintf("EMotionFX", "KdTree initialized in %f seconds (numNodes = %d numDims = %d Memory used = %.2f MB).",
initTime, m_nodes.size(),
@ -85,6 +90,7 @@ namespace EMotionFX::MotionMatching
static_cast<float>(CalcMemoryUsageInBytes()) / 1024.0f / 1024.0f);
PrintStats();
#endif
return true;
}
@ -203,6 +209,12 @@ namespace EMotionFX::MotionMatching
void KdTree::MergeSmallLeafNodesToParents()
{
// If the tree is empty or only has a single node, there is nothing to merge.
if (m_nodes.size() < 2)
{
return;
}
AZStd::vector<Node*> nodesToRemove;
for (Node* node : m_nodes)
{
@ -334,6 +346,7 @@ namespace EMotionFX::MotionMatching
void KdTree::PrintStats()
{
#if !defined(_RELEASE)
size_t leftNumFrames = 0;
size_t rightNumFrames = 0;
if (m_nodes[0]->m_leftNode)
@ -385,6 +398,7 @@ namespace EMotionFX::MotionMatching
const size_t avgFrames = (leftNumFrames + rightNumFrames) / numLeafNodes;
AZ_TracePrintf("EMotionFX", "KdTree Node Info: leafs=%d avgFrames=%d zeroFrames=%d minFrames=%d maxFrames=%d", numLeafNodes, avgFrames, numZeroNodes, minFrames, maxFrames);
#endif
}
void KdTree::FindNearestNeighbors(const AZStd::vector<float>& frameFloats, AZStd::vector<size_t>& resultFrameIndices) const
@ -410,7 +424,7 @@ namespace EMotionFX::MotionMatching
}
else
{
// We have both a left and right node, so we're not at a leaf yet.
// We have either a left and right node, so we're not at a leaf yet.
if (curNode->m_leftNode)
{
if (frameFloats[d] <= curNode->m_median)

@ -34,12 +34,10 @@ namespace EMotionFX::MotionMatching
size_t maxDepth=10,
size_t minFramesPerLeaf=1000);
/**
* Calculate the number of dimensions or values for the given feature set.
* Each feature might store one or multiple values inside the feature matrix and the number of
* values each feature holds varies with the feature type. This calculates the sum of the number of
* values of the given feature set.
*/
//! Calculate the number of dimensions or values for the given feature set.
//! Each feature might store one or multiple values inside the feature matrix and the number of
//! values each feature holds varies with the feature type. This calculates the sum of the number of
//! values of the given feature set.
static size_t CalcNumDimensions(const AZStd::vector<Feature*>& features);
void Clear();

@ -63,12 +63,12 @@ namespace EMotionFX::MotionMatching
protected:
bool ExtractFeatures(ActorInstance* actorInstance, FrameDatabase* frameDatabase, size_t maxKdTreeDepth=20, size_t minFramesPerKdTreeNode=2000);
FrameDatabase m_frameDatabase; /**< The animation database with all the keyframes and joint transform data. */
FrameDatabase m_frameDatabase; //< The animation database with all the keyframes and joint transform data.
const FeatureSchema& m_featureSchema;
FeatureMatrix m_featureMatrix;
AZStd::unique_ptr<KdTree> m_kdTree; /**< The acceleration structure to speed up the search for lowest cost frames. */
AZStd::unique_ptr<KdTree> m_kdTree; //< The acceleration structure to speed up the search for lowest cost frames.
AZStd::vector<Feature*> m_featuresInKdTree;
};
} // namespace EMotionFX::MotionMatching

@ -12,44 +12,13 @@
namespace EMotionFX::MotionMatching
{
AZ::Vector3 SampleFunction(TrajectoryQuery::EMode mode, float offset, float radius, float phase)
AZ::Vector3 SampleFunction(float offset, float radius, float phase)
{
switch (mode)
{
case TrajectoryQuery::MODE_TWO:
{
AZ::Vector3 displacement = AZ::Vector3::CreateZero();
displacement.SetX(radius * sinf(phase + offset) );
displacement.SetY(cosf(phase + offset));
return displacement;
}
case TrajectoryQuery::MODE_THREE:
{
AZ::Vector3 displacement = AZ::Vector3::CreateZero();
const float rad = radius * cosf(radius + phase*0.2f);
displacement.SetX(rad * sinf(phase + offset));
displacement.SetY(rad * cosf(phase + offset));
return displacement;
}
case TrajectoryQuery::MODE_FOUR:
{
AZ::Vector3 displacement = AZ::Vector3::CreateZero();
displacement.SetX(radius * sinf(phase + offset));
displacement.SetY(radius*2.0f * cosf(phase + offset));
return displacement;
}
// MODE_ONE and default
default:
{
AZ::Vector3 displacement = AZ::Vector3::CreateZero();
displacement.SetX(radius * sinf(phase * 0.7f + offset) + radius * 0.75f * cosf(phase * 2.0f + offset * 2.0f));
displacement.SetY(radius * cosf(phase * 0.4f + offset));
return displacement;
}
}
phase += 10.7;
AZ::Vector3 displacement = AZ::Vector3::CreateZero();
displacement.SetX(radius * sinf(phase * 0.7f + offset) + radius * 0.75f * cosf(phase * 2.0f + offset * 2.0f));
displacement.SetY(radius * cosf(phase * 0.4f + offset));
return displacement;
}
void TrajectoryQuery::Update(const ActorInstance* actorInstance,
@ -88,19 +57,18 @@ namespace EMotionFX::MotionMatching
}
else
{
static float phase = 0.0f;
phase += timeDelta * pathSpeed;
AZ::Vector3 base = SampleFunction(mode, 0.0f, pathRadius, phase);
m_automaticModePhase += timeDelta * pathSpeed;
AZ::Vector3 base = SampleFunction(0.0f, pathRadius, m_automaticModePhase);
for (size_t i = 0; i < numFutureSamples; ++i)
{
const float offset = i * 0.1f;
const AZ::Vector3 curSample = SampleFunction(mode, offset, pathRadius, phase);
const AZ::Vector3 curSample = SampleFunction(offset, pathRadius, m_automaticModePhase);
AZ::Vector3 displacement = curSample - base;
m_futureControlPoints[i].m_position = actorInstance->GetWorldSpaceTransform().m_position + displacement;
// Evaluate a control point slightly further into the future than the actual
// one and use the position difference as the facing direction.
const AZ::Vector3 deltaSample = SampleFunction(mode, offset + 0.01f, pathRadius, phase);
const AZ::Vector3 deltaSample = SampleFunction(offset + 0.01f, pathRadius, m_automaticModePhase);
const AZ::Vector3 dir = deltaSample - curSample;
m_futureControlPoints[i].m_facingDirection = dir.GetNormalizedSafe();
}

@ -36,10 +36,7 @@ namespace EMotionFX::MotionMatching
enum EMode : AZ::u8
{
MODE_TARGETDRIVEN = 0,
MODE_ONE = 1,
MODE_TWO = 2,
MODE_THREE = 3,
MODE_FOUR = 4
MODE_AUTOMATIC = 1
};
void Update(const ActorInstance* actorInstance,
@ -64,5 +61,7 @@ namespace EMotionFX::MotionMatching
AZStd::vector<ControlPoint> m_pastControlPoints;
AZStd::vector<ControlPoint> m_futureControlPoints;
float m_automaticModePhase = 0.0f; //< Current phase for the automatic demo mode. Not needed by the target-driven mode.
};
} // namespace EMotionFX::MotionMatching

@ -348,7 +348,7 @@ namespace UnitTest
{
AZ::SceneAPI::DataTypes::IMeshGroup* meshGroup = reinterpret_cast<AZ::SceneAPI::DataTypes::IMeshGroup*>(scene->GetManifest().GetValue(i).get());
AZStd::string groupName = meshGroup->GetName();
EXPECT_TRUE(groupName.starts_with("manifest_src_file_xml"));
EXPECT_TRUE(groupName.starts_with("default_mock_"));
}
}
}

@ -30,20 +30,21 @@
#include <AzToolsFramework/Prefab/PrefabSystemScriptingBus.h>
#include <AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.h>
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
#include <SceneAPI/SceneData/Rules/CoordinateSystemRule.h>
#include <SceneAPI/SceneData/Groups/MeshGroup.h>
#include <SceneAPI/SceneCore/Utilities/FileUtilities.h>
#include <SceneAPI/SceneCore/Events/ExportProductList.h>
#include <SceneAPI/SceneCore/Events/ExportEventContext.h>
#include <SceneAPI/SceneCore/Events/AssetImportRequest.h>
#include <SceneAPI/SceneCore/DataTypes/GraphData/ITransform.h>
#include <SceneAPI/SceneCore/DataTypes/GraphData/IMeshData.h>
#include <SceneAPI/SceneCore/DataTypes/DataTypeUtilities.h>
#include <SceneAPI/SceneCore/Containers/Views/SceneGraphUpwardsIterator.h>
#include <SceneAPI/SceneCore/Containers/Views/SceneGraphDownwardsIterator.h>
#include <SceneAPI/SceneCore/Containers/SceneManifest.h>
#include <SceneAPI/SceneCore/Containers/Scene.h>
#include <SceneAPI/SceneCore/Components/ExportingComponent.h>
#include <SceneAPI/SceneCore/Containers/Scene.h>
#include <SceneAPI/SceneCore/Containers/SceneManifest.h>
#include <SceneAPI/SceneCore/Containers/Views/SceneGraphDownwardsIterator.h>
#include <SceneAPI/SceneCore/Containers/Views/SceneGraphUpwardsIterator.h>
#include <SceneAPI/SceneCore/DataTypes/DataTypeUtilities.h>
#include <SceneAPI/SceneCore/DataTypes/GraphData/IMeshData.h>
#include <SceneAPI/SceneCore/DataTypes/GraphData/ITransform.h>
#include <SceneAPI/SceneCore/Events/AssetImportRequest.h>
#include <SceneAPI/SceneCore/Events/ExportEventContext.h>
#include <SceneAPI/SceneCore/Events/ExportProductList.h>
#include <SceneAPI/SceneCore/Utilities/FileUtilities.h>
#include <SceneAPI/SceneData/Groups/MeshGroup.h>
#include <SceneAPI/SceneData/Rules/CoordinateSystemRule.h>
#include <SceneAPI/SceneData/Rules/LodRule.h>
namespace AZStd
{
@ -98,6 +99,10 @@ namespace AZ::SceneAPI::Behaviors
using MeshTransformPair = AZStd::pair<Containers::SceneGraph::NodeIndex, Containers::SceneGraph::NodeIndex>;
using MeshTransformEntry = AZStd::pair<Containers::SceneGraph::NodeIndex, MeshTransformPair>;
using MeshTransformMap = AZStd::unordered_map<Containers::SceneGraph::NodeIndex, MeshTransformPair>;
using MeshIndexContainer = AZStd::unordered_set<Containers::SceneGraph::NodeIndex>;
using ManifestUpdates = AZStd::vector<AZStd::shared_ptr<DataTypes::IManifestObject>>;
using NodeEntityMap = AZStd::unordered_map<Containers::SceneGraph::NodeIndex, AZ::EntityId>;
using EntityIdList = AZStd::vector<AZ::EntityId>;
MeshTransformMap CalculateMeshTransformMap(const Containers::Scene& scene)
{
@ -108,47 +113,60 @@ namespace AZ::SceneAPI::Behaviors
graph.GetContentStorage().cbegin(),
true);
if (view.begin() == view.end())
if (view.empty())
{
return {};
}
MeshIndexContainer meshIndexContainer;
MeshTransformMap meshTransformMap;
for (auto it = view.begin(); it != view.end(); ++it)
{
Containers::SceneGraph::NodeIndex currentIndex = graph.ConvertToNodeIndex(it.GetHierarchyIterator());
AZStd::string currentNodeName = graph.GetNodeName(currentIndex).GetPath();
auto currentContent = graph.GetNodeContent(currentIndex);
const auto currentContent = graph.GetNodeContent(currentIndex);
if (currentContent)
{
if (azrtti_istypeof<AZ::SceneAPI::DataTypes::ITransform>(currentContent.get()))
{
auto parentIndex = graph.GetNodeParent(currentIndex);
const auto parentIndex = graph.GetNodeParent(currentIndex);
if (parentIndex.IsValid() == false)
{
continue;
}
auto parentContent = graph.GetNodeContent(parentIndex);
const auto parentContent = graph.GetNodeContent(parentIndex);
if (parentContent && azrtti_istypeof<AZ::SceneAPI::DataTypes::IMeshData>(parentContent.get()))
{
// map the node parent to the ITransform
meshIndexContainer.erase(parentIndex);
MeshTransformPair pair{ parentIndex, currentIndex };
meshTransformMap.emplace(MeshTransformEntry{ graph.GetNodeParent(parentIndex), AZStd::move(pair) });
}
}
else if (azrtti_istypeof<AZ::SceneAPI::DataTypes::IMeshData>(currentContent.get()))
{
meshIndexContainer.insert(currentIndex);
}
}
}
// all mesh data nodes left in the meshIndexContainer do not have a matching TransformData node
// since the nodes have an identity transform, so map the MeshData index with an Invalid mesh index to
// indicate the transform should not be set to a default value
for( const auto meshIndex : meshIndexContainer)
{
MeshTransformPair pair{ meshIndex, Containers::SceneGraph::NodeIndex{} };
meshTransformMap.emplace(MeshTransformEntry{ graph.GetNodeParent(meshIndex), AZStd::move(pair) });
}
return meshTransformMap;
}
using ManifestUpdates = AZStd::vector<AZStd::shared_ptr<DataTypes::IManifestObject>>;
using NodeEntityMap = AZStd::unordered_map<Containers::SceneGraph::NodeIndex, AZ::EntityId>;
NodeEntityMap CreateMeshGroups(
ManifestUpdates& manifestUpdates,
const MeshTransformMap& meshTransformMap,
const Containers::Scene& scene,
AZStd::string& relativeSourcePath)
const AZStd::string& relativeSourcePath)
{
NodeEntityMap nodeEntityMap;
const auto& graph = scene.GetGraph();
@ -158,15 +176,15 @@ namespace AZ::SceneAPI::Behaviors
const auto thisNodeIndex = entry.first;
const auto meshNodeIndex = entry.second.first;
const auto meshNodeName = graph.GetNodeName(meshNodeIndex);
AZStd::string meshNodePath{ meshNodeName.GetPath() };
AZStd::string meshNodeFullName;
meshNodeFullName = relativeSourcePath;
meshNodeFullName.append("_");
meshNodeFullName.append(meshNodeName.GetName());
AZStd::string meshNodePath{ meshNodeName.GetPath() };
AZStd::string meshGroupName = "default_";
meshGroupName += scene.GetName();
meshGroupName += meshNodePath;
AZ::StringFunc::Replace(meshGroupName, ".", "_");
auto meshGroup = AZStd::make_shared<AZ::SceneAPI::SceneData::MeshGroup>();
meshGroup->SetName(meshNodeFullName);
meshGroup->SetName(meshGroupName);
meshGroup->GetSceneNodeSelectionList().AddSelectedNode(AZStd::move(meshNodePath));
for (const auto& meshGoupNamePair : meshTransformMap)
{
@ -190,6 +208,9 @@ namespace AZ::SceneAPI::Behaviors
coordinateSystemRule->SetScale(1.0f);
meshGroup->GetRuleContainer().AddRule(coordinateSystemRule);
// create an empty LOD rule in order to skip the LOD buffer creation
meshGroup->GetRuleContainer().AddRule(AZStd::make_shared<AZ::SceneAPI::SceneData::LodRule>());
manifestUpdates.emplace_back(meshGroup);
// create an entity for each MeshGroup
@ -219,10 +240,15 @@ namespace AZ::SceneAPI::Behaviors
}
// assign mesh asset id hint using JSON
AZStd::string modelAssetPath;
modelAssetPath = relativeSourcePath;
AZ::StringFunc::Path::ReplaceFullName(modelAssetPath, meshGroupName.c_str());
AZ::StringFunc::Replace(modelAssetPath, "\\", "/"); // asset paths use forward slashes
auto meshAssetJson = AZStd::string::format(
R"JSON(
{"Controller": {"Configuration": {"ModelAsset": { "assetHint": "%s.azmodel"}}}}
)JSON", meshNodeFullName.c_str());
)JSON", modelAssetPath.c_str());
bool result = false;
AzToolsFramework::EntityUtilityBus::BroadcastResult(
@ -244,8 +270,6 @@ namespace AZ::SceneAPI::Behaviors
return nodeEntityMap;
}
using EntityIdList = AZStd::vector<AZ::EntityId>;
EntityIdList FixUpEntityParenting(
const NodeEntityMap& nodeEntityMap,
const Containers::SceneGraph& graph,
@ -303,7 +327,14 @@ namespace AZ::SceneAPI::Behaviors
// get node matrix data to set the entity's local transform
const auto nodeTransform = azrtti_cast<const DataTypes::ITransform*>(graph.GetNodeContent(thisTransformIndex));
entityTransform->SetLocalTM(AZ::Transform::CreateFromMatrix3x4(nodeTransform->GetMatrix()));
if (nodeTransform)
{
entityTransform->SetLocalTM(AZ::Transform::CreateFromMatrix3x4(nodeTransform->GetMatrix()));
}
else
{
entityTransform->SetLocalTM(AZ::Transform::CreateUniformScale(1.0f));
}
}
return entities;

@ -349,7 +349,10 @@ namespace AZ::SceneGenerationComponents
}
const AZStd::string name =
AZStd::string(graph.GetNodeName(nodeIndex).GetName(), graph.GetNodeName(nodeIndex).GetNameLength()).append(SceneAPI::Utilities::OptimizedMeshSuffix);
AZStd::string(graph.GetNodeName(nodeIndex).GetName(), graph.GetNodeName(nodeIndex).GetNameLength())
.append("_")
.append(meshGroup.GetName())
.append(SceneAPI::Utilities::OptimizedMeshSuffix);
if (graph.Find(name).IsValid())
{
AZ_TracePrintf(AZ::SceneAPI::Utilities::LogWindow, "Optimized mesh already exists at '%s', there must be multiple mesh groups that have selected this mesh. Skipping the additional ones.", name.c_str());

@ -79,7 +79,7 @@ namespace SceneBuilder
m_cachedFingerprint.append(element);
}
// A general catch all version fingerprint. Update this to force all FBX files to recompile.
m_cachedFingerprint.append("Version 2");
m_cachedFingerprint.append("Version 3");
}
return m_cachedFingerprint.c_str();

@ -176,7 +176,7 @@ namespace SceneProcessing
component.OptimizeMeshes(context);
AZ::SceneAPI::Containers::SceneGraph::NodeIndex optimizedNodeIndex =
graph.Find(AZStd::string("testMesh").append(AZ::SceneAPI::Utilities::OptimizedMeshSuffix));
graph.Find(AZStd::string("testMesh_").append(AZ::SceneAPI::Utilities::OptimizedMeshSuffix));
ASSERT_TRUE(optimizedNodeIndex.IsValid()) << "Mesh optimizer did not add an optimized version of the mesh";
const auto& optimizedMesh =
@ -184,7 +184,7 @@ namespace SceneProcessing
ASSERT_TRUE(optimizedMesh);
AZ::SceneAPI::Containers::SceneGraph::NodeIndex optimizedSkinDataNodeIndex =
graph.Find(AZStd::string("testMesh").append(AZ::SceneAPI::Utilities::OptimizedMeshSuffix).append(".skinWeights"));
graph.Find(AZStd::string("testMesh_").append(AZ::SceneAPI::Utilities::OptimizedMeshSuffix).append(".skinWeights"));
ASSERT_TRUE(optimizedSkinDataNodeIndex.IsValid()) << "Mesh optimizer did not add an optimized version of the skin data";
const auto& optimizedSkinWeights =
@ -227,7 +227,7 @@ namespace SceneProcessing
AZ::SceneAPI::Events::GenerateSimplificationEventContext context(scene, "pc");
component.OptimizeMeshes(context);
AZ::SceneAPI::Containers::SceneGraph::NodeIndex optimizedNodeIndex = graph.Find(AZStd::string("testMesh").append(AZ::SceneAPI::Utilities::OptimizedMeshSuffix));
AZ::SceneAPI::Containers::SceneGraph::NodeIndex optimizedNodeIndex = graph.Find(AZStd::string("testMesh_").append(AZ::SceneAPI::Utilities::OptimizedMeshSuffix));
ASSERT_TRUE(optimizedNodeIndex.IsValid()) << "Mesh optimizer did not add an optimized version of the mesh";
const auto& optimizedMesh = AZStd::rtti_pointer_cast<AZ::SceneAPI::DataTypes::IMeshData>(graph.GetNodeContent(optimizedNodeIndex));

@ -280,7 +280,7 @@ class AndroidLauncher(Launcher):
return True
return False
def kill(self):
def _kill(self):
"""
Attempts to force quit any running processes with the stored package name on the device
that is set to self._device_id via the self._adb_prefix_command

@ -208,7 +208,7 @@ class Launcher(object):
:return None:
"""
self.kill()
self._kill()
self.ensure_stopped()
self.teardown()
@ -228,7 +228,7 @@ class Launcher(object):
"""
raise NotImplementedError("Launch is not implemented")
def kill(self):
def _kill(self):
"""
Force stop the launcher.
@ -274,7 +274,7 @@ class Launcher(object):
timeout=timeout
)
except ly_test_tools.launchers.exceptions.TeardownError:
self.kill()
self._kill()
def get_device_config(self, config_file, device_section, device_key):
"""

@ -89,7 +89,7 @@ class LinuxLauncher(Launcher):
self.restore_settings()
super(LinuxLauncher, self).teardown()
def kill(self):
def _kill(self):
"""
This is a hard kill, and then wait to make sure until it actually ended.

@ -44,7 +44,7 @@ class MacLauncher(Launcher):
self._proc = subprocess.Popen(command)
log.debug(f"Started Mac Launcher with command: {command}")
def kill(self):
def _kill(self):
"""
This is a hard kill, and then wait to make sure until it actually ended.

@ -89,7 +89,7 @@ class WinLauncher(Launcher):
self.restore_settings()
super(WinLauncher, self).teardown()
def kill(self):
def _kill(self):
"""
This is a hard kill, and then wait to make sure until it actually ended.

@ -789,7 +789,7 @@ class EditorTestSuite():
test_result = Result.Fail.create(test_spec, output, editor_log_content)
except WaitTimeoutError:
output = editor.get_output()
editor.kill()
editor.stop()
editor_log_content = editor_utils.retrieve_editor_log_content(run_id, log_name, workspace)
test_result = Result.Timeout.create(test_spec, output, test_spec.timeout, editor_log_content)
@ -891,7 +891,7 @@ class EditorTestSuite():
results[test_spec_name] = Result.Crash.create(crashed_result.test_spec, output, return_code,
crash_error, crashed_result.editor_log)
except WaitTimeoutError:
editor.kill()
editor.stop()
output = editor.get_output()
editor_log_content = editor_utils.retrieve_editor_log_content(run_id, log_name, workspace)

@ -308,9 +308,11 @@ class TestAndroidLauncher:
def test_Kill_HappyPath_KillCommandSuccess(self, mock_config, mock_call):
mock_config.return_value = VALID_ANDROID_CONFIG
mock_workspace = MockedWorkspace()
launcher = ly_test_tools.launchers.AndroidLauncher(mock_workspace, ["dummy"])
launcher.kill()
# This is a direct call to a protected method, but the point of the test is to ensure functionality of this
# protected method, so we will allow this exception
launcher._kill()
mock_call.assert_called_once_with(
['adb', '-s', VALID_ANDROID_CONFIG['android']['id'], 'shell', 'am', 'force-stop', PACKAGE_NAME])

@ -52,7 +52,7 @@ class TestBaseLauncher:
def test_Kill_Unimplemented_NotImplementedError(self):
launcher = self.test_Construct_TestDoubles_BaseLauncherCreated()
with pytest.raises(NotImplementedError):
launcher.kill()
launcher.stop()
def test_Launch_Unimplemented_NotImplementedError(self):
launcher = self.test_Construct_TestDoubles_BaseLauncherCreated()
@ -111,7 +111,7 @@ class TestBaseLauncher:
with pytest.raises(NotImplementedError):
launcher.stop()
@mock.patch('ly_test_tools.launchers.platforms.base.Launcher.kill')
@mock.patch('ly_test_tools.launchers.platforms.base.Launcher._kill')
@mock.patch('ly_test_tools.launchers.platforms.base.Launcher.ensure_stopped')
@mock.patch('ly_test_tools.launchers.platforms.base.Launcher.teardown')
def test_Stop_MockImplementedLauncher_KillTeardown(self, mock_teardown, mock_ensure, mock_kill):

@ -57,7 +57,7 @@ class TestLinuxLauncher(object):
launcher = ly_test_tools.launchers.LinuxLauncher(mock.MagicMock(), ["dummy"])
launcher._proc = mock_proc
launcher.kill()
launcher.stop()
mock_proc.kill.assert_called_once()
mock_alive.assert_called_once()
mock_alive.assert_called()

@ -62,7 +62,7 @@ class TestMacLauncher(object):
launcher = ly_test_tools.launchers.MacLauncher(mock.MagicMock(), ["dummy"])
launcher._proc = mock_proc
launcher.kill()
launcher.stop()
mock_proc.kill.assert_called_once()
mock_alive.assert_called_once()
mock_alive.assert_called()

@ -56,10 +56,10 @@ class TestWinLauncher(object):
launcher = ly_test_tools.launchers.WinLauncher(mock.MagicMock(), ["dummy"])
launcher._proc = mock_proc
launcher.kill()
launcher.stop()
mock_proc.kill.assert_called_once()
mock_alive.assert_called_once()
mock_alive.assert_called()
def test_IsAlive_NoProc_False(self):
launcher = ly_test_tools.launchers.WinLauncher(mock.MagicMock(), ["dummy"])

@ -694,7 +694,7 @@ class TestRunningTests(unittest.TestCase):
'mock_log_name', mock_test_spec, [])
assert mock_cycle_crash.called
assert mock_editor.start.called
assert mock_editor.kill.called
assert mock_editor.stop.called
assert mock_create.called
assert results == {mock_test_spec.__name__: mock_timeout}

@ -56,6 +56,12 @@ include(${LY_ANDROID_NDK_TOOLCHAIN})
set(CMAKE_FIND_ROOT_PATH ${BACKUP_CMAKE_FIND_ROOT_PATH})
# The CMake Android-Initialize.cmake(https://gitlab.kitware.com/cmake/cmake/-/blob/v3.21.2/Modules/Platform/Android-Initialize.cmake#L61)
# script sets CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH to OFF resulting in find_program calls being unable to locate host binaries
# https://gitlab.kitware.com/cmake/cmake/-/issues/22634
# Setting back to true to fix our find_program calls
set(CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH ON)
# Force the ANDROID_LINKER_FLAGS that are set in the NDK's toolchain file into the LINKER_FLAGS for the build and reset
# the standard libraries
set(LINKER_FLAGS ${ANDROID_LINKER_FLAGS})

Loading…
Cancel
Save