+#include
namespace O3DE::ProjectManager
{
+ inline constexpr static int s_contentMargins = 80;
+ inline constexpr static int s_buttonSpacing = 30;
+ inline constexpr static int s_iconSize = 24;
+ inline constexpr static int s_spacerSize = 20;
+ inline constexpr static int s_boxButtonWidth = 210;
+ inline constexpr static int s_boxButtonHeight = 280;
+
FirstTimeUseScreen::FirstTimeUseScreen(QWidget* parent)
: ScreenWidget(parent)
- , m_ui(new Ui::FirstTimeUseClass())
{
- m_ui->setupUi(this);
+ QVBoxLayout* vLayout = new QVBoxLayout();
+ setLayout(vLayout);
+ vLayout->setContentsMargins(s_contentMargins, s_contentMargins, s_contentMargins, s_contentMargins);
- connect(m_ui->createProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleNewProjectButton);
- connect(m_ui->openProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleOpenProjectButton);
+ QLabel* titleLabel = new QLabel(this);
+ titleLabel->setText(tr("Ready. Set. Create!"));
+ titleLabel->setStyleSheet("font-size: 60px");
+ vLayout->addWidget(titleLabel);
+
+ QLabel* introLabel = new QLabel(this);
+ introLabel->setTextFormat(Qt::AutoText);
+ introLabel->setText(tr("Welcome to O3DE! Start something new by creating a project. Not sure what to create?
Explore what\342\200\231s available by downloading our sample project.
"));
+ introLabel->setStyleSheet("font-size: 14px");
+ vLayout->addWidget(introLabel);
+
+ QHBoxLayout* buttonLayout = new QHBoxLayout();
+ buttonLayout->setSpacing(s_buttonSpacing);
+
+ m_createProjectButton = CreateLargeBoxButton(QIcon(":/Resources/Add.svg"), tr("Create Project"), this);
+ m_createProjectButton->setIconSize(QSize(s_iconSize, s_iconSize));
+ buttonLayout->addWidget(m_createProjectButton);
+
+ m_addProjectButton = CreateLargeBoxButton(QIcon(":/Resources/Select_Folder.svg"), tr("Add a Project"), this);
+ m_addProjectButton->setIconSize(QSize(s_iconSize, s_iconSize));
+ buttonLayout->addWidget(m_addProjectButton);
+
+ QSpacerItem* buttonSpacer = new QSpacerItem(s_spacerSize, s_spacerSize, QSizePolicy::Expanding, QSizePolicy::Minimum);
+ buttonLayout->addItem(buttonSpacer);
+
+ vLayout->addItem(buttonLayout);
+
+ QSpacerItem* verticalSpacer = new QSpacerItem(s_spacerSize, s_spacerSize, QSizePolicy::Minimum, QSizePolicy::Expanding);
+ vLayout->addItem(verticalSpacer);
+
+ // Using border-image allows for scaling options background-image does not support
+ setStyleSheet("O3DE--ProjectManager--ScreenWidget { border-image: url(:/Resources/Backgrounds/FirstTimeBackgroundImage.jpg) repeat repeat; }");
+
+ connect(m_createProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleNewProjectButton);
+ connect(m_addProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleAddProjectButton);
}
ProjectManagerScreen FirstTimeUseScreen::GetScreenEnum()
@@ -36,9 +82,21 @@ namespace O3DE::ProjectManager
emit ResetScreenRequest(ProjectManagerScreen::NewProjectSettingsCore);
emit ChangeScreenRequest(ProjectManagerScreen::NewProjectSettingsCore);
}
- void FirstTimeUseScreen::HandleOpenProjectButton()
+ void FirstTimeUseScreen::HandleAddProjectButton()
{
emit ChangeScreenRequest(ProjectManagerScreen::ProjectsHome);
}
+ QPushButton* FirstTimeUseScreen::CreateLargeBoxButton(const QIcon& icon, const QString& text, QWidget* parent)
+ {
+ QPushButton* largeBoxButton = new QPushButton(icon, text, parent);
+
+ largeBoxButton->setFixedSize(s_boxButtonWidth, s_boxButtonHeight);
+ largeBoxButton->setFlat(true);
+ largeBoxButton->setFocusPolicy(Qt::FocusPolicy::NoFocus);
+ largeBoxButton->setStyleSheet("QPushButton { font-size: 14px; background-color: rgba(0, 0, 0, 191); }");
+
+ return largeBoxButton;
+ }
+
} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.h b/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.h
index 4b4a99f16a..b6b57dc16b 100644
--- a/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.h
+++ b/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.h
@@ -15,10 +15,8 @@
#include
#endif
-namespace Ui
-{
- class FirstTimeUseClass;
-}
+QT_FORWARD_DECLARE_CLASS(QIcon)
+QT_FORWARD_DECLARE_CLASS(QPushButton)
namespace O3DE::ProjectManager
{
@@ -32,10 +30,13 @@ namespace O3DE::ProjectManager
protected slots:
void HandleNewProjectButton();
- void HandleOpenProjectButton();
+ void HandleAddProjectButton();
private:
- QScopedPointer m_ui;
+ QPushButton* CreateLargeBoxButton(const QIcon& icon, const QString& text, QWidget* parent = nullptr);
+
+ QPushButton* m_createProjectButton;
+ QPushButton* m_addProjectButton;
};
} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.ui b/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.ui
deleted file mode 100644
index fdc195731f..0000000000
--- a/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.ui
+++ /dev/null
@@ -1,93 +0,0 @@
-
-
- FirstTimeUseClass
-
-
-
- 0
- 0
- 881
- 555
-
-
-
- Form
-
-
- -
-
-
-
-
-
-
- 30
-
-
-
- READY. SET. CREATE!
-
-
-
- -
-
-
- <html><head/><body><p>Welcome to O3DE! Start something new by creating a project. Not sure what to create? </p><p>Explore what’s available by downloading our sample project.</p></body></html>
-
-
- Qt::AutoText
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- Create Project
-
-
-
- :/Resources/Add.svg:/Resources/Add.svg
-
-
-
- 16
- 16
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Open a Project
-
-
-
- :/Resources/Select_Folder.svg:/Resources/Select_Folder.svg
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp
index 977667071f..6b9d268564 100644
--- a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp
+++ b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp
@@ -27,6 +27,12 @@ namespace O3DE::ProjectManager
, m_ui(new Ui::ProjectManagerWindowClass())
{
m_ui->setupUi(this);
+ QLayout* layout = m_ui->centralWidget->layout();
+ layout->setMargin(0);
+ layout->setSpacing(0);
+ layout->setContentsMargins(0, 0, 0, 0);
+
+ setFixedSize(this->geometry().width(), this->geometry().height());
m_pythonBindings = AZStd::make_unique(engineRootPath);
diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui
index 789dd1b656..a71ed3aabf 100644
--- a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui
+++ b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui
@@ -6,10 +6,16 @@
0
0
- 800
- 600
+ 1200
+ 800
+
+
+ 0
+ 0
+
+
O3DE Project Manager
@@ -21,7 +27,7 @@
0
0
- 800
+ 1200
36
diff --git a/Code/Tools/ProjectManager/Source/ScreenWidget.h b/Code/Tools/ProjectManager/Source/ScreenWidget.h
index ae235daf2b..483066e031 100644
--- a/Code/Tools/ProjectManager/Source/ScreenWidget.h
+++ b/Code/Tools/ProjectManager/Source/ScreenWidget.h
@@ -15,18 +15,20 @@
#include
#include
+#include
+#include
#endif
namespace O3DE::ProjectManager
{
class ScreenWidget
- : public QWidget
+ : public QFrame
{
Q_OBJECT
public:
explicit ScreenWidget(QWidget* parent = nullptr)
- : QWidget(parent)
+ : QFrame(parent)
{
}
~ScreenWidget() = default;
diff --git a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp
index 69af09f496..b8a38ed155 100644
--- a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp
+++ b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp
@@ -22,6 +22,9 @@ namespace O3DE::ProjectManager
: QWidget(parent)
{
QVBoxLayout* vLayout = new QVBoxLayout();
+ vLayout->setMargin(0);
+ vLayout->setSpacing(0);
+ vLayout->setContentsMargins(0, 0, 0, 0);
setLayout(vLayout);
m_screenStack = new QStackedWidget();
diff --git a/Code/Tools/ProjectManager/project_manager.qrc b/Code/Tools/ProjectManager/project_manager.qrc
index 6509a9f940..3c23bc24ff 100644
--- a/Code/Tools/ProjectManager/project_manager.qrc
+++ b/Code/Tools/ProjectManager/project_manager.qrc
@@ -9,5 +9,6 @@
Resources/iOS.svg
Resources/Linux.svg
Resources/macOS.svg
+ Resources/Backgrounds/FirstTimeBackgroundImage.jpg
diff --git a/Code/Tools/ProjectManager/project_manager_files.cmake b/Code/Tools/ProjectManager/project_manager_files.cmake
index 9ffdb6029d..3594d1e079 100644
--- a/Code/Tools/ProjectManager/project_manager_files.cmake
+++ b/Code/Tools/ProjectManager/project_manager_files.cmake
@@ -22,7 +22,6 @@ set(FILES
Source/EngineInfo.cpp
Source/FirstTimeUseScreen.h
Source/FirstTimeUseScreen.cpp
- Source/FirstTimeUseScreen.ui
Source/ProjectManagerWindow.h
Source/ProjectManagerWindow.cpp
Source/ProjectTemplateInfo.h
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp
index 0ee25195bc..38f7de89c6 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp
@@ -260,7 +260,7 @@ namespace AZ
{
AZ_TraceContext("Importer", "Animation");
- aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
+ const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
// Add check for animation layers at the scene level.
@@ -387,11 +387,10 @@ namespace AZ
}
Events::ProcessingResultCombiner combinedAnimationResult;
- for (AZ::u32 meshIndex = 0; meshIndex < currentNode->mNumMeshes; ++meshIndex)
+ if (context.m_sourceNode.ContainsMesh())
{
- aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[meshIndex]];
-
- if (NodeToChannelToMorphAnim::iterator channelsForMeshName = meshMorphAnimations.find(mesh->mName.C_Str());
+ const aiMesh* firstMesh = scene->mMeshes[currentNode->mMeshes[0]];
+ if (NodeToChannelToMorphAnim::iterator channelsForMeshName = meshMorphAnimations.find(firstMesh->mName.C_Str());
channelsForMeshName != meshMorphAnimations.end())
{
const auto [nodeIterName, channels] = *channelsForMeshName;
@@ -399,7 +398,7 @@ namespace AZ
{
const auto& [animation, morphAnimation] = animAndMorphAnim;
combinedAnimationResult += ImportBlendShapeAnimation(
- context, animation, morphAnimation, mesh);
+ context, animation, morphAnimation, firstMesh);
}
}
}
@@ -413,32 +412,39 @@ namespace AZ
if (boneAnimations.empty() && !meshMorphAnimations.empty())
{
const aiAnimation* animation = scene->mAnimations[0];
-
- // Morph animations need a regular animation on the node, as well.
- // If there is no bone animation on the current node, then generate one here.
- AZStd::shared_ptr createdAnimationData =
- AZStd::make_shared();
-
- const size_t numKeyframes = animation->mDuration + 1; // +1 because we start at 0 and the last keyframe is at mDuration instead of mDuration-1
- createdAnimationData->ReserveKeyFrames(numKeyframes);
-
- const double timeStepBetweenFrames = 1.0 / animation->mTicksPerSecond;
- createdAnimationData->SetTimeStepBetweenFrames(timeStepBetweenFrames);
-
- // Set every frame of the animation to the start location of the node.
- aiMatrix4x4 combinedTransform = GetConcatenatedLocalTransform(currentNode);
- DataTypes::MatrixType localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(combinedTransform);
- context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform);
- context.m_sourceSceneSystem.ConvertUnit(localTransform);
- for (AZ::u32 time = 0; time <= animation->mDuration; ++time)
+ for (AZ::u32 channelIndex = 0; channelIndex < animation->mNumMorphMeshChannels; ++channelIndex)
{
- createdAnimationData->AddKeyFrame(localTransform);
+ const aiMeshMorphAnim* nodeAnim = animation->mMorphMeshChannels[channelIndex];
+ // Morph animations need a regular animation on the node, as well.
+ // If there is no bone animation on the current node, then generate one here.
+ AZStd::shared_ptr createdAnimationData =
+ AZStd::make_shared();
+
+ const size_t numKeyframes = GetNumKeyFrames(
+ nodeAnim->mNumKeys,
+ animation->mDuration,
+ animation->mTicksPerSecond);
+ createdAnimationData->ReserveKeyFrames(numKeyframes);
+
+ const double timeStepBetweenFrames = 1.0 / animation->mTicksPerSecond;
+ createdAnimationData->SetTimeStepBetweenFrames(timeStepBetweenFrames);
+
+ // Set every frame of the animation to the start location of the node.
+ aiMatrix4x4 combinedTransform = GetConcatenatedLocalTransform(currentNode);
+ DataTypes::MatrixType localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(combinedTransform);
+ context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform);
+ context.m_sourceSceneSystem.ConvertUnit(localTransform);
+ for (AZ::u32 time = 0; time <= numKeyframes; ++time)
+ {
+ createdAnimationData->AddKeyFrame(localTransform);
+ }
+
+ const AZStd::string stubBoneAnimForMorphName(AZStd::string::format("%s%s", nodeName.c_str(), nodeAnim->mName.C_Str()));
+ Containers::SceneGraph::NodeIndex addNode = context.m_scene.GetGraph().AddChild(
+ context.m_currentGraphPosition, stubBoneAnimForMorphName.c_str(), AZStd::move(createdAnimationData));
+ context.m_scene.GetGraph().MakeEndPoint(addNode);
}
-
- Containers::SceneGraph::NodeIndex addNode = context.m_scene.GetGraph().AddChild(
- context.m_currentGraphPosition, nodeName.c_str(), AZStd::move(createdAnimationData));
- context.m_scene.GetGraph().MakeEndPoint(addNode);
-
+
return combinedAnimationResult.GetResult();
}
decltype(boneAnimations) parentFillerAnimations;
@@ -446,8 +452,8 @@ namespace AZ
// Go through all the animations and make sure we create animations for bones who's parents don't have an animation
for (auto&& anim : boneAnimations)
{
- aiNode* node = scene->mRootNode->FindNode(anim.first.c_str());
- aiNode* parent = node->mParent;
+ const aiNode* node = scene->mRootNode->FindNode(anim.first.c_str());
+ const aiNode* parent = node->mParent;
while (parent && parent != scene->mRootNode)
{
@@ -598,7 +604,8 @@ namespace AZ
// Keyframes generated for every single frame of the animation.
typedef AZStd::map> ValueToKeyDataMap;
ValueToKeyDataMap valueToKeyDataMap;
-
+ // Key time can be less than zero, normalize to have zero be the lowest time.
+ double keyOffset = 0;
for (int keyIdx = 0; keyIdx < meshMorphAnim->mNumKeys; keyIdx++)
{
aiMeshMorphKey& key = meshMorphAnim->mKeys[keyIdx];
@@ -609,6 +616,10 @@ namespace AZ
valueToKeyDataMap[currentValue].insert(
AZStd::upper_bound(valueToKeyDataMap[currentValue].begin(), valueToKeyDataMap[currentValue].end(),thisKey),
thisKey);
+ if (key.mTime < keyOffset)
+ {
+ keyOffset = key.mTime;
+ }
}
}
@@ -631,7 +642,7 @@ namespace AZ
const double time = GetTimeForFrame(frame, animation->mTicksPerSecond);
float weight = 0;
- if (!SampleKeyFrame(weight, keys, keys.size(), time, keyIdx))
+ if (!SampleKeyFrame(weight, keys, keys.size(), time + keyOffset, keyIdx))
{
return Events::ProcessingResult::Failure;
}
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBitangentStreamImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBitangentStreamImporter.cpp
index c2b1f20035..2ce9bc14f4 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBitangentStreamImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBitangentStreamImporter.cpp
@@ -25,7 +25,6 @@
#include
#include
-
namespace AZ
{
namespace SceneAPI
@@ -44,7 +43,7 @@ namespace AZ
SerializeContext* serializeContext = azrtti_cast(context);
if (serializeContext)
{
- serializeContext->Class()->Version(2); // LYN-2576
+ serializeContext->Class()->Version(3); // LYN-3250
}
}
@@ -55,62 +54,79 @@ namespace AZ
{
return Events::ProcessingResult::Ignored;
}
- aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
+ const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
- GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
- if (!meshDataResult.IsSuccess())
+ const auto meshHasTangentsAndBitangents = [&scene](const unsigned int meshIndex)
{
- return meshDataResult.GetError();
- }
- const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
+ return scene->mMeshes[meshIndex]->HasTangentsAndBitangents();
+ };
- size_t vertexCount = parentMeshData->GetVertexCount();
-
- int sdkMeshIndex = parentMeshData->GetSdkMeshIndex();
- if (sdkMeshIndex < 0 || sdkMeshIndex >= currentNode->mNumMeshes)
- {
- AZ_Error(Utilities::ErrorWindow, false,
- "Tried to construct bitangent stream attribute for invalid or non-mesh parent data, mesh index is invalid");
- return Events::ProcessingResult::Failure;
- }
-
- aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
-
- if (!mesh->HasTangentsAndBitangents())
+ // If there are no bitangents on any meshes, there's nothing to import in this function.
+ const bool anyMeshHasTangentsAndBitangents = AZStd::any_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents);
+ if (!anyMeshHasTangentsAndBitangents)
{
return Events::ProcessingResult::Ignored;
}
+ // AssImp nodes with multiple meshes on them occur when AssImp split a mesh on material.
+ // This logic recombines those meshes to minimize the changes needed to replace FBX SDK with AssImp, FBX SDK did not separate meshes,
+ // and the engine has code to do this later.
+ const bool allMeshesHaveTangentsAndBitangents = AZStd::all_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents);
+ if (!allMeshesHaveTangentsAndBitangents)
+ {
+ const char* mixedBitangentsError =
+ "Node with name %s has meshes with and without bitangents. "
+ "Placeholder incorrect bitangents will be generated to allow the data to process, "
+ "but the source art needs to be fixed to correct this. Either apply bitangents to all meshes on this node, "
+ "or remove all bitangents from all meshes on this node.";
+ AZ_Error(
+ Utilities::ErrorWindow, false, mixedBitangentsError, currentNode->mName.C_Str());
+ }
+
+ const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
+
AZStd::shared_ptr bitangentStream =
AZStd::make_shared();
-
// AssImp only has one bitangentStream per mesh.
bitangentStream->SetBitangentSetIndex(0);
bitangentStream->SetTangentSpace(AZ::SceneAPI::DataTypes::TangentSpace::FromFbx);
bitangentStream->ReserveContainerSpace(vertexCount);
-
- for (int v = 0; v < mesh->mNumVertices; ++v)
+ for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex)
{
- const Vector3 bitangent(
- AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mBitangents[v]));
- bitangentStream->AppendBitangent(bitangent);
+ const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
+
+ for (int v = 0; v < mesh->mNumVertices; ++v)
+ {
+ if (!mesh->HasTangentsAndBitangents())
+ {
+ // This node has mixed meshes with and without bitangents.
+ // An error was already thrown above. Output stub bitangents so
+ // the mesh can still be output in some form, even if the data isn't correct.
+ // The bitangent count needs to match the vertex count on the associated mesh node.
+ bitangentStream->AppendBitangent(Vector3::CreateAxisY());
+ }
+ else
+ {
+ const Vector3 bitangent(
+ AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mBitangents[v]));
+ bitangentStream->AppendBitangent(bitangent);
+ }
+ }
}
- AZStd::string nodeName(AZStd::string::format("%s",m_defaultNodeName));
Containers::SceneGraph::NodeIndex newIndex =
- context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
+ context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, m_defaultNodeName);
Events::ProcessingResult bitangentResults;
- AssImpSceneAttributeDataPopulatedContext dataPopulated(context, bitangentStream, newIndex, nodeName.c_str());
+ AssImpSceneAttributeDataPopulatedContext dataPopulated(context, bitangentStream, newIndex, m_defaultNodeName);
bitangentResults = Events::Process(dataPopulated);
if (bitangentResults != Events::ProcessingResult::Failure)
{
bitangentResults = AddAttributeDataNodeWithContexts(dataPopulated);
}
-
return bitangentResults;
}
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBlendShapeImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBlendShapeImporter.cpp
index c0399329d3..34266a3e29 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBlendShapeImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBlendShapeImporter.cpp
@@ -74,37 +74,51 @@ namespace AZ
{
return meshDataResult.GetError();
}
- const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
- int parentMeshIndex = parentMeshData->GetSdkMeshIndex();
Events::ProcessingResultCombiner combinedBlendShapeResult;
+ // 1. Loop through meshes & anims
+ // Create storage: Anim to meshes
+ // 2. Loop through anims & meshes
+ // Create an anim mesh for each anim, with meshes re-combined.
+ // AssImp separates meshes that have multiple materials.
+ // This code re-combines them to match previous FBX SDK behavior,
+ // so they can be separated by engine code instead.
+ AZStd::map>> animToMeshToAnimMeshIndices;
for (int nodeMeshIdx = 0; nodeMeshIdx < numMesh; nodeMeshIdx++)
{
int sceneMeshIdx = context.m_sourceNode.GetAssImpNode()->mMeshes[nodeMeshIdx];
const aiMesh* aiMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[sceneMeshIdx];
-
- // Each mesh gets its own node in the scene graph, so only generate
- // morph targets for the current mesh.
- if (parentMeshIndex != nodeMeshIdx || !aiMesh->mNumAnimMeshes)
- {
- continue;
- }
-
for (int animIdx = 0; animIdx < aiMesh->mNumAnimMeshes; animIdx++)
{
- AZStd::shared_ptr blendShapeData =
- AZStd::make_shared();
-
aiAnimMesh* aiAnimMesh = aiMesh->mAnimMeshes[animIdx];
- AZStd::string nodeName(aiAnimMesh->mName.C_Str());
- size_t dotIndex = nodeName.rfind('.');
- if (dotIndex != AZStd::string::npos)
- {
- nodeName.erase(0, dotIndex + 1);
- }
- RenamedNodesMap::SanitizeNodeName(nodeName, context.m_scene.GetGraph(), context.m_currentGraphPosition, "BlendShape");
- AZ_TraceContext("Blend shape name", nodeName);
+ animToMeshToAnimMeshIndices[aiAnimMesh->mName.C_Str()].emplace_back(nodeMeshIdx, animIdx);
+ }
+ }
+
+ for (const auto& animToMeshIndex : animToMeshToAnimMeshIndices)
+ {
+ AZStd::shared_ptr blendShapeData =
+ AZStd::make_shared();
+
+ // Some DCC tools, like Maya, include a full path separated by '.' in the node names.
+ // For example, "cone_skin_blendShapeNode.cone_squash"
+ // Downstream processing doesn't want anything but the last part of that node name,
+ // so find the last '.' and remove anything before it.
+ AZStd::string nodeName(animToMeshIndex.first);
+ size_t dotIndex = nodeName.rfind('.');
+ if (dotIndex != AZStd::string::npos)
+ {
+ nodeName.erase(0, dotIndex + 1);
+ }
+ int vertexOffset = 0;
+ RenamedNodesMap::SanitizeNodeName(nodeName, context.m_scene.GetGraph(), context.m_currentGraphPosition, "BlendShape");
+ AZ_TraceContext("Blend shape name", nodeName);
+ for (const auto& meshIndex : animToMeshIndex.second)
+ {
+ int sceneMeshIdx = context.m_sourceNode.GetAssImpNode()->mMeshes[meshIndex.first];
+ const aiMesh* aiMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[sceneMeshIdx];
+ const aiAnimMesh* aiAnimMesh = aiMesh->mAnimMeshes[meshIndex.second];
AZStd::bitset uvSetUsedFlags;
for (AZ::u8 uvSetIndex = 0; uvSetIndex < SceneData::GraphData::BlendShapeData::MaxNumUVSets; ++uvSetIndex)
@@ -128,7 +142,7 @@ namespace AZ
context.m_sourceSceneSystem.ConvertUnit(vertex);
blendShapeData->AddPosition(vertex);
- blendShapeData->SetVertexIndexToControlPointIndexMap(vertIdx, vertIdx);
+ blendShapeData->SetVertexIndexToControlPointIndexMap(vertIdx + vertexOffset, vertIdx + vertexOffset);
// Add normals
if (aiAnimMesh->HasNormals())
@@ -191,33 +205,36 @@ namespace AZ
}
for (int idx = 0; idx < face.mNumIndices; ++idx)
{
- blendFace.vertexIndex[idx] = face.mIndices[idx];
+ blendFace.vertexIndex[idx] = face.mIndices[idx] + vertexOffset;
}
blendShapeData->AddFace(blendFace);
}
+ vertexOffset += aiMesh->mNumVertices;
- // Report problem if no vertex or face converted to MeshData
- if (blendShapeData->GetVertexCount() <= 0 || blendShapeData->GetFaceCount() <= 0)
- {
- AZ_Error(Utilities::ErrorWindow, false, "Missing geometry data in blendshape node %s.", nodeName.c_str());
- return Events::ProcessingResult::Failure;
- }
- Containers::SceneGraph::NodeIndex newIndex =
- context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
-
- Events::ProcessingResult blendShapeResult;
- AssImpSceneAttributeDataPopulatedContext dataPopulated(context, blendShapeData, newIndex, nodeName);
- blendShapeResult = Events::Process(dataPopulated);
-
- if (blendShapeResult != Events::ProcessingResult::Failure)
- {
- blendShapeResult = AddAttributeDataNodeWithContexts(dataPopulated);
- }
- combinedBlendShapeResult += blendShapeResult;
}
+
+ // Report problem if no vertex or face converted to MeshData
+ if (blendShapeData->GetVertexCount() <= 0 || blendShapeData->GetFaceCount() <= 0)
+ {
+ AZ_Error(Utilities::ErrorWindow, false, "Missing geometry data in blendshape node %s.", nodeName.c_str());
+ return Events::ProcessingResult::Failure;
+ }
+
+ Containers::SceneGraph::NodeIndex newIndex =
+ context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
+
+ Events::ProcessingResult blendShapeResult;
+ AssImpSceneAttributeDataPopulatedContext dataPopulated(context, blendShapeData, newIndex, nodeName);
+ blendShapeResult = Events::Process(dataPopulated);
+
+ if (blendShapeResult != Events::ProcessingResult::Failure)
+ {
+ blendShapeResult = AddAttributeDataNodeWithContexts(dataPopulated);
+ }
+ combinedBlendShapeResult += blendShapeResult;
}
return combinedBlendShapeResult.GetResult();
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp
index 4467d6933b..5b43941715 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp
@@ -46,8 +46,8 @@ namespace AZ
}
void EnumBonesInNode(
- const aiScene* scene, const aiNode* node, AZStd::unordered_map& mainBoneList,
- AZStd::unordered_map& boneLookup)
+ const aiScene* scene, const aiNode* node, AZStd::unordered_map& mainBoneList,
+ AZStd::unordered_map& boneLookup)
{
/* From AssImp Documentation
a) Create a map or a similar container to store which nodes are necessary for the skeleton. Pre-initialise it for all nodes with a "no".
@@ -62,14 +62,14 @@ namespace AZ
for (unsigned meshIndex = 0; meshIndex < node->mNumMeshes; ++meshIndex)
{
- aiMesh* mesh = scene->mMeshes[node->mMeshes[meshIndex]];
+ const aiMesh* mesh = scene->mMeshes[node->mMeshes[meshIndex]];
for (unsigned boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex)
{
- aiBone* bone = mesh->mBones[boneIndex];
+ const aiBone* bone = mesh->mBones[boneIndex];
- aiNode* boneNode = scene->mRootNode->FindNode(bone->mName);
- aiNode* boneParent = boneNode->mParent;
+ const aiNode* boneNode = scene->mRootNode->FindNode(bone->mName);
+ const aiNode* boneParent = boneNode->mParent;
mainBoneList[bone->mName.C_Str()] = boneNode;
boneLookup[bone->mName.C_Str()] = bone;
@@ -85,8 +85,8 @@ namespace AZ
}
void EnumChildren(
- const aiScene* scene, const aiNode* node, AZStd::unordered_map& mainBoneList,
- AZStd::unordered_map& boneLookup)
+ const aiScene* scene, const aiNode* node, AZStd::unordered_map& mainBoneList,
+ AZStd::unordered_map& boneLookup)
{
EnumBonesInNode(scene, node, mainBoneList, boneLookup);
@@ -102,7 +102,7 @@ namespace AZ
{
AZ_TraceContext("Importer", "Bone");
- aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
+ const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
if (IsPivotNode(currentNode->mName))
@@ -118,8 +118,8 @@ namespace AZ
}
else
{
- AZStd::unordered_map mainBoneList;
- AZStd::unordered_map boneLookup;
+ AZStd::unordered_map mainBoneList;
+ AZStd::unordered_map boneLookup;
EnumChildren(scene, scene->mRootNode, mainBoneList, boneLookup);
if (mainBoneList.find(currentNode->mName.C_Str()) != mainBoneList.end())
@@ -172,7 +172,7 @@ namespace AZ
}
aiMatrix4x4 transform = currentNode->mTransformation;
- aiNode* parent = currentNode->mParent;
+ const aiNode* parent = currentNode->mParent;
while (parent)
{
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpColorStreamImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpColorStreamImporter.cpp
index 7ebdb55363..75fa39105f 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpColorStreamImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpColorStreamImporter.cpp
@@ -11,6 +11,7 @@
*/
#include
+#include
#include
#include
#include
@@ -44,7 +45,7 @@ namespace AZ
SerializeContext* serializeContext = azrtti_cast(context);
if (serializeContext)
{
- serializeContext->Class()->Version(2); // LYN-2576
+ serializeContext->Class()->Version(3); // LYN-3250
}
}
@@ -55,43 +56,64 @@ namespace AZ
{
return Events::ProcessingResult::Ignored;
}
- aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
+ const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
- GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
- if (!meshDataResult.IsSuccess())
- {
- return meshDataResult.GetError();
- }
- const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
+ // This node has at least one mesh, verify that the color channel counts are the same for all meshes.
+ const int expectedColorChannels = scene->mMeshes[currentNode->mMeshes[0]]->GetNumColorChannels();
+ const bool allMeshesHaveSameNumberOfColorChannels =
+ AZStd::all_of(currentNode->mMeshes + 1, currentNode->mMeshes + currentNode->mNumMeshes, [scene, expectedColorChannels](const unsigned int meshIndex)
+ {
+ return scene->mMeshes[meshIndex]->GetNumColorChannels() == expectedColorChannels;
+ });
- size_t vertexCount = parentMeshData->GetVertexCount();
+ AZ_Error(
+ Utilities::ErrorWindow,
+ allMeshesHaveSameNumberOfColorChannels,
+ "Color channel counts for node %s has meshes with different color channel counts. "
+ "The color channel count for the first mesh will be used, and placeholder incorrect color values "
+ "will be generated to allow the data to process, but the source art needs to be fixed to correct this. "
+ "All meshes on this node should have the same number of color channels.",
+ currentNode->mName.C_Str());
- int sdkMeshIndex = parentMeshData->GetSdkMeshIndex();
- if (sdkMeshIndex < 0)
+ if (expectedColorChannels == 0)
{
- AZ_Error(Utilities::ErrorWindow, false,
- "Tried to construct color stream attribute for invalid or non-mesh parent data, mesh index is missing");
- return Events::ProcessingResult::Failure;
+ return Events::ProcessingResult::Ignored;
}
- aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
+ const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
Events::ProcessingResultCombiner combinedVertexColorResults;
- for (int colorSetIndex = 0; colorSetIndex < mesh->GetNumColorChannels(); ++colorSetIndex)
+ for (int colorSetIndex = 0; colorSetIndex < expectedColorChannels; ++colorSetIndex)
{
+
AZStd::shared_ptr vertexColors =
AZStd::make_shared();
vertexColors->ReserveContainerSpace(vertexCount);
- for (int v = 0; v < mesh->mNumVertices; ++v)
+ for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex)
{
- AZ::SceneAPI::DataTypes::Color vertexColor(
- AssImpSDKWrapper::AssImpTypeConverter::ToColor(mesh->mColors[colorSetIndex][v]));
- vertexColors->AppendColor(vertexColor);
+ const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
+ for (int v = 0; v < mesh->mNumVertices; ++v)
+ {
+ if (colorSetIndex < mesh->GetNumColorChannels())
+ {
+ AZ::SceneAPI::DataTypes::Color vertexColor(
+ AssImpSDKWrapper::AssImpTypeConverter::ToColor(mesh->mColors[colorSetIndex][v]));
+ vertexColors->AppendColor(vertexColor);
+ }
+ else
+ {
+ // An error was already emitted if this mesh has less color channels
+ // than other meshes on the parent node. Append an arbitrary color value, fully opaque black,
+ // so the mesh can still be processed.
+ // It's better to let the engine load a partially valid mesh than to completely fail.
+ vertexColors->AppendColor(AZ::SceneAPI::DataTypes::Color(0.0f,0.0f,0.0f,1.0f));
+ }
+ }
}
- AZStd::string nodeName(AZStd::string::format("%s%d",m_defaultNodeName,colorSetIndex));
+ AZStd::string nodeName(AZStd::string::format("%s%d", m_defaultNodeName, colorSetIndex));
Containers::SceneGraph::NodeIndex newIndex =
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
@@ -106,9 +128,7 @@ namespace AZ
combinedVertexColorResults += colorMapResults;
}
-
return combinedVertexColorResults.GetResult();
-
}
} // namespace FbxSceneBuilder
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpImporterUtilities.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpImporterUtilities.cpp
index 80ac01ebdf..e79eaa09aa 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpImporterUtilities.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpImporterUtilities.cpp
@@ -69,7 +69,7 @@ namespace AZ
aiMatrix4x4 GetConcatenatedLocalTransform(const aiNode* currentNode)
{
- aiNode* parent = currentNode->mParent;
+ const aiNode* parent = currentNode->mParent;
aiMatrix4x4 combinedTransform = currentNode->mTransformation;
while (parent)
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMaterialImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMaterialImporter.cpp
index e314804ea1..a912b90e34 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMaterialImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMaterialImporter.cpp
@@ -62,7 +62,7 @@ namespace AZ
for (int idx = 0; idx < context.m_sourceNode.m_assImpNode->mNumMeshes; ++idx)
{
int meshIndex = context.m_sourceNode.m_assImpNode->mMeshes[idx];
- aiMesh* assImpMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[meshIndex];
+ const aiMesh* assImpMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[meshIndex];
AZ_Assert(assImpMesh, "Asset Importer Mesh should not be null.");
int materialIndex = assImpMesh->mMaterialIndex;
AZ_TraceContext("Material Index", materialIndex);
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMeshImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMeshImporter.cpp
index cafb96934d..193a1f9fd5 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMeshImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMeshImporter.cpp
@@ -45,7 +45,7 @@ namespace AZ
{
AZ_TraceContext("Importer", "Mesh");
- aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
+ const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
if (!context.m_sourceNode.ContainsMesh() || IsSkinnedMesh(*currentNode, *scene))
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinImporter.cpp
index 145dc9a457..f4a5fd0f93 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinImporter.cpp
@@ -45,7 +45,7 @@ namespace AZ
{
AZ_TraceContext("Importer", "Skin");
- aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
+ const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
if (!context.m_sourceNode.ContainsMesh() || !IsSkinnedMesh(*currentNode, *scene))
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.cpp
index abcbf10b4a..d8503857cc 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.cpp
@@ -51,7 +51,7 @@ namespace AZ
{
AZ_TraceContext("Importer", "Skin Weights");
- aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
+ const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
if(currentNode->mNumMeshes <= 0)
@@ -59,35 +59,21 @@ namespace AZ
return Events::ProcessingResult::Ignored;
}
- GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
- if (!meshDataResult.IsSuccess())
- {
- return meshDataResult.GetError();
- }
- const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
-
- int parentMeshIndex = parentMeshData->GetSdkMeshIndex();
-
Events::ProcessingResultCombiner combinedSkinWeightsResult;
+ // Don't create this until a bone with weights is encountered
+ Containers::SceneGraph::NodeIndex weightsIndexForMesh;
+ AZStd::string skinWeightName;
+ AZStd::shared_ptr skinWeightData;
+
+ const uint64_t totalVertices = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
+
+ int vertexCount = 0;
for(unsigned nodeMeshIndex = 0; nodeMeshIndex < currentNode->mNumMeshes; ++nodeMeshIndex)
{
- if (nodeMeshIndex != parentMeshIndex)
- {
- // Only generate skinning data for the parent mesh.
- // Each AssImp mesh is assigned to a unique node,
- // so the skinning data should be generated as a child node
- // for the associated parent mesh.
- continue;
- }
int sceneMeshIndex = currentNode->mMeshes[nodeMeshIndex];
const aiMesh* mesh = scene->mMeshes[sceneMeshIndex];
- // Don't create this until a bone with weights is encountered
- Containers::SceneGraph::NodeIndex weightsIndexForMesh;
- AZStd::string skinWeightName;
- AZStd::shared_ptr skinWeightData;
-
for(unsigned b = 0; b < mesh->mNumBones; ++b)
{
const aiBone* bone = mesh->mBones[b];
@@ -100,7 +86,6 @@ namespace AZ
if (!weightsIndexForMesh.IsValid())
{
skinWeightName = s_skinWeightName;
- skinWeightName += AZStd::to_string(nodeMeshIndex);
RenamedNodesMap::SanitizeNodeName(skinWeightName, context.m_scene.GetGraph(), context.m_currentGraphPosition);
weightsIndexForMesh =
@@ -116,23 +101,25 @@ namespace AZ
}
Pending pending;
pending.m_bone = bone;
- pending.m_numVertices = mesh->mNumVertices;
+ pending.m_numVertices = totalVertices;
pending.m_skinWeightData = skinWeightData;
+ pending.m_vertOffset = vertexCount;
m_pendingSkinWeights.push_back(pending);
}
-
- Events::ProcessingResult skinWeightsResult;
- AssImpSceneAttributeDataPopulatedContext dataPopulated(context, skinWeightData, weightsIndexForMesh, skinWeightName);
- skinWeightsResult = Events::Process(dataPopulated);
-
- if (skinWeightsResult != Events::ProcessingResult::Failure)
- {
- skinWeightsResult = AddAttributeDataNodeWithContexts(dataPopulated);
- }
-
- combinedSkinWeightsResult += skinWeightsResult;
+ vertexCount += mesh->mNumVertices;
}
+ Events::ProcessingResult skinWeightsResult;
+ AssImpSceneAttributeDataPopulatedContext dataPopulated(context, skinWeightData, weightsIndexForMesh, skinWeightName);
+ skinWeightsResult = Events::Process(dataPopulated);
+
+ if (skinWeightsResult != Events::ProcessingResult::Failure)
+ {
+ skinWeightsResult = AddAttributeDataNodeWithContexts(dataPopulated);
+ }
+
+ combinedSkinWeightsResult += skinWeightsResult;
+
return combinedSkinWeightsResult.GetResult();
}
@@ -153,7 +140,7 @@ namespace AZ
link.boneId = boneId;
link.weight = it.m_bone->mWeights[weight].mWeight;
- it.m_skinWeightData->AddAndSortLink(it.m_bone->mWeights[weight].mVertexId, link);
+ it.m_skinWeightData->AddAndSortLink(it.m_bone->mWeights[weight].mVertexId + it.m_vertOffset, link);
}
}
const auto result = m_pendingSkinWeights.empty() ? Events::ProcessingResult::Ignored : Events::ProcessingResult::Success;
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.h b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.h
index f048fe0af2..655c838701 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.h
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.h
@@ -61,6 +61,7 @@ namespace AZ
{
const aiBone* m_bone = nullptr;
unsigned m_numVertices = 0;
+ unsigned m_vertOffset = 0;
AZStd::shared_ptr m_skinWeightData;
};
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTangentStreamImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTangentStreamImporter.cpp
index 992a6a6ab1..47b7e410b4 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTangentStreamImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTangentStreamImporter.cpp
@@ -11,6 +11,7 @@
*/
#include
+#include
#include
#include
#include
@@ -44,7 +45,7 @@ namespace AZ
SerializeContext* serializeContext = azrtti_cast(context);
if (serializeContext)
{
- serializeContext->Class()->Version(2); // LYN-2576
+ serializeContext->Class()->Version(3); // LYN-3250
}
}
@@ -55,62 +56,79 @@ namespace AZ
{
return Events::ProcessingResult::Ignored;
}
- aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
+ const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
-
- GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
- if (!meshDataResult.IsSuccess())
+
+ const auto meshHasTangentsAndBitangents = [&scene](const unsigned int meshIndex)
{
- return meshDataResult.GetError();
- }
- const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
+ return scene->mMeshes[meshIndex]->HasTangentsAndBitangents();
+ };
- size_t vertexCount = parentMeshData->GetVertexCount();
-
- int sdkMeshIndex = parentMeshData->GetSdkMeshIndex();
- if (sdkMeshIndex < 0 || sdkMeshIndex >= currentNode->mNumMeshes)
- {
- AZ_Error(Utilities::ErrorWindow, false,
- "Tried to construct tangent stream attribute for invalid or non-mesh parent data, mesh index is invalid");
- return Events::ProcessingResult::Failure;
- }
-
- aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
-
- if (!mesh->HasTangentsAndBitangents())
+ // If there are no tangents on any meshes, there's nothing to import in this function.
+ const bool anyMeshHasTangentsAndBitangents = AZStd::any_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents);
+ if (!anyMeshHasTangentsAndBitangents)
{
return Events::ProcessingResult::Ignored;
}
+ // AssImp nodes with multiple meshes on them occur when AssImp split a mesh on material.
+ // This logic recombines those meshes to minimize the changes needed to replace FBX SDK with AssImp, FBX SDK did not separate meshes,
+ // and the engine has code to do this later.
+ const bool allMeshesHaveTangentsAndBitangents = AZStd::all_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents);
+ if (!allMeshesHaveTangentsAndBitangents)
+ {
+ const char* mixedTangentsError =
+ "Node with name %s has meshes with and without tangents. "
+ "Placeholder incorrect tangents will be generated to allow the data to process, "
+ "but the source art needs to be fixed to correct this. Either apply tangents to all meshes on this node, "
+ "or remove all tangents from all meshes on this node.";
+ AZ_Error(
+ Utilities::ErrorWindow, false, mixedTangentsError, currentNode->mName.C_Str());
+ }
+
+ const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
+
AZStd::shared_ptr tangentStream =
AZStd::make_shared();
-
// AssImp only has one tangentStream per mesh.
tangentStream->SetTangentSetIndex(0);
tangentStream->SetTangentSpace(AZ::SceneAPI::DataTypes::TangentSpace::FromFbx);
tangentStream->ReserveContainerSpace(vertexCount);
-
- for (int v = 0; v < mesh->mNumVertices; ++v)
+ for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex)
{
- // Vector4's constructor that takes in a vector3 sets w to 1.0f automatically.
- const Vector4 tangent(AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mTangents[v]));
- tangentStream->AppendTangent(tangent);
+ const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
+
+ for (int v = 0; v < mesh->mNumVertices; ++v)
+ {
+ if (!mesh->HasTangentsAndBitangents())
+ {
+ // This node has mixed meshes with and without tangents.
+ // An error was already thrown above. Output stub tangents so
+ // the mesh can still be output in some form, even if the data isn't correct.
+ // The tangent count needs to match the vertex count on the associated mesh node.
+ tangentStream->AppendTangent(Vector4(0.f, 1.f, 0.f, 1.f));
+ }
+ else
+ {
+ const Vector4 tangent(
+ AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mTangents[v]));
+ tangentStream->AppendTangent(tangent);
+ }
+ }
}
- AZStd::string nodeName(AZStd::string::format("%s", m_defaultNodeName));
Containers::SceneGraph::NodeIndex newIndex =
- context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
+ context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, m_defaultNodeName);
Events::ProcessingResult tangentResults;
- AssImpSceneAttributeDataPopulatedContext dataPopulated(context, tangentStream, newIndex, nodeName.c_str());
+ AssImpSceneAttributeDataPopulatedContext dataPopulated(context, tangentStream, newIndex, m_defaultNodeName);
tangentResults = Events::Process(dataPopulated);
if (tangentResults != Events::ProcessingResult::Failure)
{
tangentResults = AddAttributeDataNodeWithContexts(dataPopulated);
}
-
return tangentResults;
}
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp
index 84c0e3e18c..5357c32fa9 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp
@@ -50,7 +50,7 @@ namespace AZ
Events::ProcessingResult AssImpTransformImporter::ImportTransform(AssImpSceneNodeAppendedContext& context)
{
AZ_TraceContext("Importer", "transform");
- aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
+ const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
if (currentNode == scene->mRootNode || IsPivotNode(currentNode->mName))
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpUvMapImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpUvMapImporter.cpp
index f5f47b233d..e37a4f4285 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpUvMapImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpUvMapImporter.cpp
@@ -12,17 +12,19 @@
#include
#include
+#include
+#include
#include
#include
#include
#include
#include
#include
-#include
-#include
+#include
#include
#include
-#include
+#include
+#include
#include
#include
@@ -45,7 +47,7 @@ namespace AZ
SerializeContext* serializeContext = azrtti_cast(context);
if (serializeContext)
{
- serializeContext->Class()->Version(3); // LYN-2506
+ serializeContext->Class()->Version(4); // LYN-3250
}
}
@@ -56,28 +58,53 @@ namespace AZ
{
return Events::ProcessingResult::Ignored;
}
- aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
+ const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
- GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
- if (!meshDataResult.IsSuccess())
+ // AssImp separates meshes that have multiple materials.
+ // This code re-combines them to match previous FBX SDK behavior,
+ // so they can be separated by engine code instead.
+ bool foundTextureCoordinates = false;
+ AZStd::array meshesPerTextureCoordinateIndex = {};
+ for (int localMeshIndex = 0; localMeshIndex < currentNode->mNumMeshes; ++localMeshIndex)
{
- return meshDataResult.GetError();
+ aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[localMeshIndex]];
+ for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex)
+ {
+ if (!mesh->mTextureCoords[texCoordIndex])
+ {
+ continue;
+ }
+ ++meshesPerTextureCoordinateIndex[texCoordIndex];
+ foundTextureCoordinates = true;
+ }
}
- const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
- size_t vertexCount = parentMeshData->GetVertexCount();
+ if (!foundTextureCoordinates)
+ {
+ return Events::ProcessingResult::Ignored;
+ }
- int sdkMeshIndex = parentMeshData->GetSdkMeshIndex();
- AZ_Assert(sdkMeshIndex >= 0,
- "Tried to construct uv stream attribute for invalid or non-mesh parent data, mesh index is missing");
+ const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
- aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
+ for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex)
+ {
+ int meshesWithIndex = meshesPerTextureCoordinateIndex[texCoordIndex];
+ AZ_Error(
+ Utilities::ErrorWindow,
+ meshesWithIndex == 0 || meshesWithIndex == currentNode->mNumMeshes,
+ "Texture coordinate index %d for node %s is not on all meshes on this node. "
+ "Placeholder arbitrary texture values will be generated to allow the data to process, but the source art "
+ "needs to be fixed to correct this. All meshes on this node should have the same number of texture coordinate channels.",
+ texCoordIndex,
+ currentNode->mName.C_Str());
+ }
Events::ProcessingResultCombiner combinedUvMapResults;
- for (int texCoordIndex = 0; texCoordIndex < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++texCoordIndex)
+ for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex)
{
- if (!mesh->mTextureCoords[texCoordIndex])
+ // No meshes have this texture coordinate index, skip it.
+ if (meshesPerTextureCoordinateIndex[texCoordIndex] == 0)
{
continue;
}
@@ -85,24 +112,55 @@ namespace AZ
AZStd::shared_ptr uvMap =
AZStd::make_shared();
uvMap->ReserveContainerSpace(vertexCount);
-
+ bool customNameFound = false;
AZStd::string name(AZStd::string::format("%s%d", m_defaultNodeName, texCoordIndex));
- if (mesh->mTextureCoordsNames[texCoordIndex].length)
+ for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex)
{
- name = mesh->mTextureCoordsNames[texCoordIndex].C_Str();
+ const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
+ if(mesh->mTextureCoords[texCoordIndex])
+ {
+ if (mesh->mTextureCoordsNames[texCoordIndex].length > 0)
+ {
+ if (!customNameFound)
+ {
+ name = mesh->mTextureCoordsNames[texCoordIndex].C_Str();
+ customNameFound = true;
+ }
+ else
+ {
+ AZ_Warning(Utilities::WarningWindow,
+ strcmp(name.c_str(), mesh->mTextureCoordsNames[texCoordIndex].C_Str()) == 0,
+ "Node %s has conflicting mesh coordinate names at index %d, %s and %s. Using %s.",
+ currentNode->mName.C_Str(),
+ texCoordIndex,
+ name.c_str(),
+ mesh->mTextureCoordsNames[texCoordIndex].C_Str(),
+ name.c_str());
+ }
+ }
+ }
+
+ for (int v = 0; v < mesh->mNumVertices; ++v)
+ {
+ if (mesh->mTextureCoords[texCoordIndex])
+ {
+ AZ::Vector2 vertexUV(
+ mesh->mTextureCoords[texCoordIndex][v].x,
+ // The engine's V coordinate is reverse of how it's stored in the FBX file.
+ 1.0f - mesh->mTextureCoords[texCoordIndex][v].y);
+ uvMap->AppendUV(vertexUV);
+ }
+ else
+ {
+ // An error was already emitted if the UV channels for all meshes on this node do not match.
+ // Append an arbitrary UV value so that the mesh can still be processed.
+ // It's better to let the engine load a partially valid mesh than to completely fail.
+ uvMap->AppendUV(AZ::Vector2::CreateZero());
+ }
+ }
}
uvMap->SetCustomName(name.c_str());
-
- for (int v = 0; v < mesh->mNumVertices; ++v)
- {
- AZ::Vector2 vertexUV(
- mesh->mTextureCoords[texCoordIndex][v].x,
- // The engine's V coordinate is reverse of how it's stored in the FBX file.
- 1.0f - mesh->mTextureCoords[texCoordIndex][v].y);
- uvMap->AppendUV(vertexUV);
- }
-
Containers::SceneGraph::NodeIndex newIndex =
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, name.c_str());
@@ -116,6 +174,7 @@ namespace AZ
}
combinedUvMapResults += uvMapResults;
+
}
return combinedUvMapResults.GetResult();
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.cpp
index 59821336e0..c90fe7d1f3 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.cpp
@@ -13,6 +13,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -24,7 +25,7 @@
namespace AZ::SceneAPI::FbxSceneBuilder
{
- bool BuildSceneMeshFromAssImpMesh(aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector>& meshes,
+ bool BuildSceneMeshFromAssImpMesh(const aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector>& meshes,
const AZStd::function()>& makeMeshFunc)
{
AZStd::unordered_map assImpMatIndexToLYIndex;
@@ -34,17 +35,18 @@ namespace AZ::SceneAPI::FbxSceneBuilder
{
return false;
}
+ auto newMesh = makeMeshFunc();
+ newMesh->SetUnitSizeInMeters(sceneSystem.GetUnitSizeInMeters());
+ newMesh->SetOriginalUnitSizeInMeters(sceneSystem.GetOriginalUnitSizeInMeters());
+
+ // AssImp separates meshes that have multiple materials.
+ // This code re-combines them to match previous FBX SDK behavior,
+ // so they can be separated by engine code instead.
+ int vertOffset = 0;
for (int m = 0; m < currentNode->mNumMeshes; ++m)
{
- auto newMesh = makeMeshFunc();
-
- newMesh->SetUnitSizeInMeters(sceneSystem.GetUnitSizeInMeters());
- newMesh->SetOriginalUnitSizeInMeters(sceneSystem.GetOriginalUnitSizeInMeters());
-
- newMesh->SetSdkMeshIndex(m);
-
- aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[m]];
+ const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[m]];
// Lumberyard materials are created in order based on mesh references in the scene
if (assImpMatIndexToLYIndex.find(mesh->mMaterialIndex) == assImpMatIndexToLYIndex.end())
@@ -59,7 +61,7 @@ namespace AZ::SceneAPI::FbxSceneBuilder
sceneSystem.SwapVec3ForUpAxis(vertex);
sceneSystem.ConvertUnit(vertex);
newMesh->AddPosition(vertex);
- newMesh->SetVertexIndexToControlPointIndexMap(vertIdx, vertIdx);
+ newMesh->SetVertexIndexToControlPointIndexMap(vertIdx + vertOffset, vertIdx + vertOffset);
if (mesh->HasNormals())
{
@@ -86,14 +88,15 @@ namespace AZ::SceneAPI::FbxSceneBuilder
}
for (int idx = 0; idx < face.mNumIndices; ++idx)
{
- meshFace.vertexIndex[idx] = face.mIndices[idx];
+ meshFace.vertexIndex[idx] = face.mIndices[idx] + vertOffset;
}
newMesh->AddFace(meshFace, assImpMatIndexToLYIndex[mesh->mMaterialIndex]);
}
+ vertOffset += mesh->mNumVertices;
- meshes.push_back(newMesh);
}
+ meshes.push_back(newMesh);
return true;
}
@@ -127,4 +130,13 @@ namespace AZ::SceneAPI::FbxSceneBuilder
azrtti_cast(parentData);
return AZ::Success(parentMeshData);
}
+
+ uint64_t GetVertexCountForAllMeshesOnNode(const aiNode& node, const aiScene& scene)
+ {
+ return AZStd::accumulate(node.mMeshes, node.mMeshes + node.mNumMeshes, uint64_t{ 0u },
+ [&scene](auto runningTotal, unsigned int meshIndex)
+ {
+ return runningTotal + scene.mMeshes[meshIndex]->mNumVertices;
+ });
+ }
}
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.h b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.h
index c0b5c044cb..3c7d3d2102 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.h
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.h
@@ -44,11 +44,16 @@ namespace AZ
namespace FbxSceneBuilder
{
- bool BuildSceneMeshFromAssImpMesh(aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector>& meshes,
+ bool BuildSceneMeshFromAssImpMesh(const aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector>& meshes,
const AZStd::function()>& makeMeshFunc);
typedef AZ::Outcome GetMeshDataFromParentResult;
GetMeshDataFromParentResult GetMeshDataFromParent(AssImpSceneNodeAppendedContext& context);
+
+ // If a node in the original scene file has a mesh with multiple materials on it, the associated AssImp
+ // node will have multiple meshes on it, broken apart per material. This returns the total number
+ // of vertices on all meshes on the given node.
+ uint64_t GetVertexCountForAllMeshesOnNode(const aiNode& node, const aiScene& scene);
}
}
}
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/RenamedNodesMap.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/RenamedNodesMap.cpp
index abc095d583..b67696e3cc 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/RenamedNodesMap.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/RenamedNodesMap.cpp
@@ -27,6 +27,7 @@ namespace AZ
Containers::SceneGraph::NodeIndex parentNode, const char* defaultName)
{
AZ_TraceContext("Node name", name);
+ const AZStd::string originalNodeName(name);
bool isNameUpdated = false;
// Nodes can't have an empty name, except of the root, otherwise nodes can't be referenced.
@@ -56,7 +57,7 @@ namespace AZ
// can't reference the same parent in that case. This is to make sure the node can be quickly found as
// the full path will be unique. To fix any issues, an index is appended.
size_t index = 1;
- size_t offset = name.length();
+ const size_t offset = name.length();
while (graph.Find(parentNode, name).IsValid())
{
// Remove the previously tried extension.
@@ -71,7 +72,8 @@ namespace AZ
if (isNameUpdated)
{
AZ_TraceContext("New node name", name);
- AZ_TracePrintf(Utilities::WarningWindow, "The name of the node was invalid or conflicting and was updated.");
+ AZ_TracePrintf(Utilities::WarningWindow, "The name of the node '%s' was invalid or conflicting and was updated to '%s'.",
+ originalNodeName.c_str(), name.c_str());
}
return isNameUpdated;
diff --git a/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp b/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp
index eccbe729c6..2cda1e68ae 100644
--- a/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp
+++ b/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp
@@ -38,12 +38,14 @@ namespace AZ
{
AZ_TracePrintf(SceneAPI::Utilities::LogWindow, "AssImpSceneWrapper::LoadSceneFromFile %s", fileName);
AZ_TraceContext("Filename", fileName);
+ // aiProcess_JoinIdenticalVertices is not enabled because O3DE has a mesh optimizer that also does this,
+ // this flag is disabled to keep AssImp output similar to FBX SDK to reduce downstream bugs for the initial AssImp release.
+ // There's currently a minimum of properties and flags set to maximize compatibility with the existing node graph.
m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false);
m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, false);
m_sceneFileName = fileName;
m_assImpScene = m_importer.ReadFile(fileName,
aiProcess_Triangulate //Triangulates all faces of all meshes
- | aiProcess_JoinIdenticalVertices //Identifies and joins identical vertex data sets for the imported meshes
| aiProcess_LimitBoneWeights //Limits the number of bones that can affect a vertex to a maximum value
//dropping the least important and re-normalizing
| aiProcess_GenNormals); //Generate normals for meshes
diff --git a/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.cpp b/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.cpp
index e99788d570..f3c01df2e3 100644
--- a/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.cpp
+++ b/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.cpp
@@ -19,6 +19,16 @@ namespace AZ::SceneAPI::Utilities
m_output += AZStd::string::format("\t%s: %s\n", name, data);
}
+ void DebugOutput::WriteArray(const char* name, const unsigned int* data, int size)
+ {
+ m_output += AZStd::string::format("\t%s: ", name);
+ for (int index = 0; index < size; ++index)
+ {
+ m_output += AZStd::string::format("%d, ", data[index]);
+ }
+ m_output += AZStd::string::format("\n");
+ }
+
void DebugOutput::Write(const char* name, const AZStd::string& data)
{
Write(name, data.c_str());
diff --git a/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.h b/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.h
index a598c996c9..83dc9dd6ab 100644
--- a/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.h
+++ b/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.h
@@ -29,6 +29,7 @@ namespace AZ::SceneAPI::Utilities
void Write(const char* name, const AZStd::vector>& data);
SCENE_CORE_API void Write(const char* name, const char* data);
+ SCENE_CORE_API void WriteArray(const char* name, const unsigned int* data, int size);
SCENE_CORE_API void Write(const char* name, const AZStd::string& data);
SCENE_CORE_API void Write(const char* name, double data);
SCENE_CORE_API void Write(const char* name, uint64_t data);
diff --git a/Code/Tools/SceneAPI/SceneData/GraphData/BlendShapeData.cpp b/Code/Tools/SceneAPI/SceneData/GraphData/BlendShapeData.cpp
index 902928d404..7d81166829 100644
--- a/Code/Tools/SceneAPI/SceneData/GraphData/BlendShapeData.cpp
+++ b/Code/Tools/SceneAPI/SceneData/GraphData/BlendShapeData.cpp
@@ -285,8 +285,26 @@ namespace AZ
void BlendShapeData::GetDebugOutput(SceneAPI::Utilities::DebugOutput& output) const
{
output.Write("Positions", m_positions);
+ int index = 0;
+ for (const auto& position : m_positions)
+ {
+ output.Write(AZStd::string::format("\t%d", index).c_str(), position);
+ ++index;
+ }
+ index = 0;
output.Write("Normals", m_normals);
+ for (const auto& normal : m_normals)
+ {
+ output.Write(AZStd::string::format("\t%d", index).c_str(), normal);
+ ++index;
+ }
+ index = 0;
output.Write("Faces", m_faces);
+ for (const auto& face : m_faces)
+ {
+ output.WriteArray(AZStd::string::format("\t%d", index).c_str(), face.vertexIndex, 3);
+ ++index;
+ }
}
} // GraphData
} // SceneData
diff --git a/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.cpp b/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.cpp
index 8bf49b2898..8f464240c9 100644
--- a/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.cpp
+++ b/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.cpp
@@ -45,7 +45,6 @@ namespace AZ
behaviorContext->Class()
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Module, "scene")
- ->Method("GetSdkMeshIndex", &MeshData::GetSdkMeshIndex)
->Method("GetControlPointIndex", &MeshData::GetControlPointIndex)
->Method("GetUsedControlPointCount", &MeshData::GetUsedControlPointCount)
->Method("GetUsedPointIndexForControlPoint", &MeshData::GetUsedPointIndexForControlPoint)
@@ -77,10 +76,6 @@ namespace AZ
void MeshData::CloneAttributesFrom(const IGraphObject* sourceObject)
{
IMeshData::CloneAttributesFrom(sourceObject);
- if (const auto* typedSource = azrtti_cast(sourceObject))
- {
- SetSdkMeshIndex(typedSource->GetSdkMeshIndex());
- }
}
void MeshData::AddPosition(const AZ::Vector3& position)
@@ -111,15 +106,6 @@ namespace AZ
m_faceMaterialIds.push_back(faceMaterialId);
}
- void MeshData::SetSdkMeshIndex(int sdkMeshIndex)
- {
- m_sdkMeshIndex = sdkMeshIndex;
- }
- int MeshData::GetSdkMeshIndex() const
- {
- return m_sdkMeshIndex;
- }
-
void MeshData::SetVertexIndexToControlPointIndexMap(int vertexIndex, int controlPointIndex)
{
m_vertexIndexToControlPointIndexMap[vertexIndex] = controlPointIndex;
@@ -206,8 +192,26 @@ namespace AZ
void MeshData::GetDebugOutput(SceneAPI::Utilities::DebugOutput& output) const
{
output.Write("Positions", m_positions);
+ int index = 0;
+ for (const auto& position : m_positions)
+ {
+ output.Write(AZStd::string::format("\t%d", index).c_str(), position);
+ ++index;
+ }
+ index = 0;
output.Write("Normals", m_normals);
+ for (const auto& normal : m_normals)
+ {
+ output.Write(AZStd::string::format("\t%d", index).c_str(), normal);
+ ++index;
+ }
+ index = 0;
output.Write("FaceList", m_faceList);
+ for (const auto& face : m_faceList)
+ {
+ output.WriteArray(AZStd::string::format("\t%d", index).c_str(), face.vertexIndex, 3);
+ ++index;
+ }
output.Write("FaceMaterialIds", m_faceMaterialIds);
}
}
diff --git a/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.h b/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.h
index c5197d6cb4..4321096857 100644
--- a/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.h
+++ b/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.h
@@ -49,9 +49,6 @@ namespace AZ
SCENE_DATA_API void AddFace(const AZ::SceneAPI::DataTypes::IMeshData::Face& face,
unsigned int faceMaterialId = AZ::SceneAPI::DataTypes::IMeshData::s_invalidMaterialId);
- SCENE_DATA_API void SetSdkMeshIndex(int sdkMeshIndex);
- SCENE_DATA_API int GetSdkMeshIndex() const;
-
SCENE_DATA_API void SetVertexIndexToControlPointIndexMap(int vertexIndex, int controlPointIndex);
SCENE_DATA_API size_t GetUsedControlPointCount() const override;
SCENE_DATA_API int GetControlPointIndex(int vertexIndex) const override;
@@ -80,8 +77,6 @@ namespace AZ
AZStd::unordered_map m_vertexIndexToControlPointIndexMap;
AZStd::unordered_map m_controlPointToUsedVertexIndexMap;
-
- int m_sdkMeshIndex = -1;
};
}
}
diff --git a/Code/Tools/SceneAPI/SceneData/Tests/GraphData/GraphDataBehaviorTests.cpp b/Code/Tools/SceneAPI/SceneData/Tests/GraphData/GraphDataBehaviorTests.cpp
index 79c6cfea7e..84018bbc9a 100644
--- a/Code/Tools/SceneAPI/SceneData/Tests/GraphData/GraphDataBehaviorTests.cpp
+++ b/Code/Tools/SceneAPI/SceneData/Tests/GraphData/GraphDataBehaviorTests.cpp
@@ -57,7 +57,6 @@ namespace AZ
meshData->AddNormal(Vector3{0.1f, 0.2f, 0.3f});
meshData->AddNormal(Vector3{0.4f, 0.5f, 0.6f});
meshData->SetOriginalUnitSizeInMeters(10.0f);
- meshData->SetSdkMeshIndex(1337);
meshData->SetUnitSizeInMeters(0.5f);
meshData->SetVertexIndexToControlPointIndexMap(0, 10);
meshData->SetVertexIndexToControlPointIndexMap(1, 11);
@@ -252,7 +251,6 @@ namespace AZ
ExpectExecute("TestExpectFloatEquals(meshData:GetNormal(1).z, 0.6)");
ExpectExecute("TestExpectFloatEquals(meshData:GetOriginalUnitSizeInMeters(), 10.0)");
ExpectExecute("TestExpectFloatEquals(meshData:GetUnitSizeInMeters(), 0.5)");
- ExpectExecute("TestExpectIntegerEquals(meshData:GetSdkMeshIndex(), 1337)");
ExpectExecute("TestExpectIntegerEquals(meshData:GetUsedControlPointCount(), 4)");
ExpectExecute("TestExpectIntegerEquals(meshData:GetControlPointIndex(0), 10)");
ExpectExecute("TestExpectIntegerEquals(meshData:GetControlPointIndex(1), 11)");
diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.azsl
index 84095ac163..456d7cbabe 100644
--- a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.azsl
+++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.azsl
@@ -101,7 +101,7 @@ struct VSOutput
float2 m_uv[UvSetCount] : UV1;
float2 m_detailUv : UV3;
- float4 m_blendMask : UV8;
+ float4 m_wrinkleBlendFactors : UV8;
};
#include
@@ -132,11 +132,11 @@ VSOutput SkinVS(VSInput IN)
if(o_blendMask_isBound)
{
- OUT.m_blendMask = IN.m_optional_blendMask;
+ OUT.m_wrinkleBlendFactors = IN.m_optional_blendMask;
}
else
{
- OUT.m_blendMask = float4(0,1,0,0);
+ OUT.m_wrinkleBlendFactors = float4(0,0,0,0);
}
VertexHelper(IN, OUT, worldPosition, false);
@@ -214,7 +214,22 @@ PbrLightingOutput SkinPS_Common(VSOutput IN)
float2 normalUv = IN.m_uv[MaterialSrg::m_normalMapUvIndex];
float detailLayerNormalFactor = MaterialSrg::m_detail_normal_factor * detailLayerBlendFactor;
-
+
+ // ------- Wrinkle Map Setup -------
+
+ // Combine the optional per-morph target wrinkle masks
+ float4 wrinkleBlendFactors = float4(0.0, 0.0, 0.0, 0.0);
+ for(uint wrinkleMaskIndex = 0; wrinkleMaskIndex < ObjectSrg::m_wrinkle_mask_count; ++wrinkleMaskIndex)
+ {
+ wrinkleBlendFactors += ObjectSrg::m_wrinkle_masks[wrinkleMaskIndex].Sample(MaterialSrg::m_sampler, normalUv) * ObjectSrg::GetWrinkleMaskWeight(wrinkleMaskIndex);
+ }
+
+ // If texture based morph target driven masks are being used, use those values instead of the per-vertex colors
+ if(ObjectSrg::m_wrinkle_mask_count)
+ {
+ IN.m_wrinkleBlendFactors = saturate(wrinkleBlendFactors);
+ }
+
// Since the wrinkle normal maps should all be in the same tangent space as the main normal map, we should be able to blend the raw normal map
// texture values before doing all the tangent space transforms, so we only have to do the transforms once, for better performance.
@@ -223,12 +238,12 @@ PbrLightingOutput SkinPS_Common(VSOutput IN)
{
normalMapSample = SampleNormalXY(MaterialSrg::m_normalMap, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY);
}
- if(o_wrinkleLayers_enabled && o_blendMask_isBound && o_wrinkleLayers_normal_enabled)
+ if(o_wrinkleLayers_enabled && o_wrinkleLayers_normal_enabled)
{
- normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture1, normalMapSample, MaterialSrg::m_wrinkle_normal_texture1, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_blendMask.r);
- normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture2, normalMapSample, MaterialSrg::m_wrinkle_normal_texture2, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_blendMask.g);
- normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture3, normalMapSample, MaterialSrg::m_wrinkle_normal_texture3, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_blendMask.b);
- normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture4, normalMapSample, MaterialSrg::m_wrinkle_normal_texture4, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_blendMask.a);
+ normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture1, normalMapSample, MaterialSrg::m_wrinkle_normal_texture1, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_wrinkleBlendFactors.r);
+ normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture2, normalMapSample, MaterialSrg::m_wrinkle_normal_texture2, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_wrinkleBlendFactors.g);
+ normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture3, normalMapSample, MaterialSrg::m_wrinkle_normal_texture3, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_wrinkleBlendFactors.b);
+ normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture4, normalMapSample, MaterialSrg::m_wrinkle_normal_texture4, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_wrinkleBlendFactors.a);
}
if(o_detail_normal_useTexture)
@@ -255,7 +270,7 @@ PbrLightingOutput SkinPS_Common(VSOutput IN)
float3 baseColor = GetBaseColorInput(MaterialSrg::m_baseColorMap, MaterialSrg::m_sampler, baseColorUv, MaterialSrg::m_baseColor, o_baseColor_useTexture);
bool useSampledBaseColor = o_baseColor_useTexture;
- if(o_wrinkleLayers_enabled && o_blendMask_isBound && o_wrinkleLayers_baseColor_enabled)
+ if(o_wrinkleLayers_enabled && o_wrinkleLayers_baseColor_enabled)
{
// If any of the wrinkle maps are applied, we will use the Base Color blend settings to apply the MaterialSrg::m_baseColor tint to the wrinkle maps,
// even if the main base color map is not used.
@@ -272,10 +287,10 @@ PbrLightingOutput SkinPS_Common(VSOutput IN)
baseColor = float3(1,1,1);
}
- baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture1, baseColor, MaterialSrg::m_wrinkle_baseColor_texture1, MaterialSrg::m_sampler, baseColorUv, IN.m_blendMask.r);
- baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture2, baseColor, MaterialSrg::m_wrinkle_baseColor_texture2, MaterialSrg::m_sampler, baseColorUv, IN.m_blendMask.g);
- baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture3, baseColor, MaterialSrg::m_wrinkle_baseColor_texture3, MaterialSrg::m_sampler, baseColorUv, IN.m_blendMask.b);
- baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture4, baseColor, MaterialSrg::m_wrinkle_baseColor_texture4, MaterialSrg::m_sampler, baseColorUv, IN.m_blendMask.a);
+ baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture1, baseColor, MaterialSrg::m_wrinkle_baseColor_texture1, MaterialSrg::m_sampler, baseColorUv, IN.m_wrinkleBlendFactors.r);
+ baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture2, baseColor, MaterialSrg::m_wrinkle_baseColor_texture2, MaterialSrg::m_sampler, baseColorUv, IN.m_wrinkleBlendFactors.g);
+ baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture3, baseColor, MaterialSrg::m_wrinkle_baseColor_texture3, MaterialSrg::m_sampler, baseColorUv, IN.m_wrinkleBlendFactors.b);
+ baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture4, baseColor, MaterialSrg::m_wrinkle_baseColor_texture4, MaterialSrg::m_sampler, baseColorUv, IN.m_wrinkleBlendFactors.a);
}
@@ -283,13 +298,13 @@ PbrLightingOutput SkinPS_Common(VSOutput IN)
baseColor = ApplyTextureOverlay(o_detail_baseColor_useTexture, baseColor, MaterialSrg::m_detail_baseColor_texture, MaterialSrg::m_sampler, IN.m_detailUv, detailLayerBaseColorFactor);
- if(o_wrinkleLayers_enabled && o_wrinkleLayers_showBlendMaskValues && o_blendMask_isBound)
+ if(o_wrinkleLayers_enabled && o_wrinkleLayers_showBlendMaskValues)
{
// Overlay debug colors to highlight the different blend weights coming from the vertex color stream.
- if(o_wrinkleLayers_count > 0) { baseColor = lerp(baseColor, float3(1,0,0), IN.m_blendMask.r); }
- if(o_wrinkleLayers_count > 1) { baseColor = lerp(baseColor, float3(0,1,0), IN.m_blendMask.g); }
- if(o_wrinkleLayers_count > 2) { baseColor = lerp(baseColor, float3(0,0,1), IN.m_blendMask.b); }
- if(o_wrinkleLayers_count > 3) { baseColor = lerp(baseColor, float3(1,1,1), IN.m_blendMask.a); }
+ if(o_wrinkleLayers_count > 0) { baseColor = lerp(baseColor, float3(1,0,0), IN.m_wrinkleBlendFactors.r); }
+ if(o_wrinkleLayers_count > 1) { baseColor = lerp(baseColor, float3(0,1,0), IN.m_wrinkleBlendFactors.g); }
+ if(o_wrinkleLayers_count > 2) { baseColor = lerp(baseColor, float3(0,0,1), IN.m_wrinkleBlendFactors.b); }
+ if(o_wrinkleLayers_count > 3) { baseColor = lerp(baseColor, float3(1,1,1), IN.m_wrinkleBlendFactors.a); }
}
// ------- Specular -------
diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype
index b8951d69c7..101a03b907 100644
--- a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype
+++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype
@@ -987,15 +987,6 @@
{
"file": "Shaders/MotionVector/SkinnedMeshMotionVector.shader",
"tag": "SkinnedMeshMotionVector"
- },
- // Used by the light culling system to produce accurate depth bounds for this object when it uses blended transparency
- {
- "file": "Shaders/Depth/DepthPassTransparentMin.shader",
- "tag": "DepthPassTransparentMin"
- },
- {
- "file": "Shaders/Depth/DepthPassTransparentMax.shader",
- "tag": "DepthPassTransparentMax"
}
],
"functors": [
diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype
index a74ceb1783..2b0d09bc5c 100644
--- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype
+++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype
@@ -1199,6 +1199,14 @@
"file": "./StandardPBR_ForwardPass_EDS.shader",
"tag": "ForwardPass_EDS"
},
+ {
+ "file": "./StandardPBR_LowEndForward.shader",
+ "tag": "LowEndForward"
+ },
+ {
+ "file": "./StandardPBR_LowEndForward_EDS.shader",
+ "tag": "LowEndForward_EDS"
+ },
{
"file": "Shaders/Shadow/Shadowmap.shader",
"tag": "Shadowmap"
@@ -1289,10 +1297,6 @@
"textureProperty": "baseColor.textureMap",
"useTextureProperty": "baseColor.useTexture",
"dependentProperties": ["baseColor.textureMapUv", "baseColor.textureBlendMode"],
- "shaderTags": [
- "ForwardPass",
- "ForwardPass_EDS"
- ],
"shaderOption": "o_baseColor_useTexture"
}
},
@@ -1302,10 +1306,6 @@
"textureProperty": "metallic.textureMap",
"useTextureProperty": "metallic.useTexture",
"dependentProperties": ["metallic.textureMapUv"],
- "shaderTags": [
- "ForwardPass",
- "ForwardPass_EDS"
- ],
"shaderOption": "o_metallic_useTexture"
}
},
@@ -1315,10 +1315,6 @@
"textureProperty": "specularF0.textureMap",
"useTextureProperty": "specularF0.useTexture",
"dependentProperties": ["specularF0.textureMapUv"],
- "shaderTags": [
- "ForwardPass",
- "ForwardPass_EDS"
- ],
"shaderOption": "o_specularF0_useTexture"
}
},
@@ -1328,10 +1324,6 @@
"textureProperty": "normal.textureMap",
"useTextureProperty": "normal.useTexture",
"dependentProperties": ["normal.textureMapUv", "normal.factor", "normal.flipX", "normal.flipY"],
- "shaderTags": [
- "ForwardPass",
- "ForwardPass_EDS"
- ],
"shaderOption": "o_normal_useTexture"
}
},
diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl
index f362349a7b..bf1ec96b79 100644
--- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl
+++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl
@@ -10,6 +10,8 @@
*
*/
+#include "Atom/Features/ShaderQualityOptions.azsli"
+
#include "StandardPBR_Common.azsli"
// SRGs
@@ -317,13 +319,18 @@ ForwardPassOutputWithDepth StandardPbr_ForwardPassPS(VSOutput IN, bool isFrontFa
PbrLightingOutput lightingOutput = ForwardPassPS_Common(IN, isFrontFace, depth);
+#ifdef UNIFIED_FORWARD_OUTPUT
+ OUT.m_color.rgb = lightingOutput.m_diffuseColor.rgb + lightingOutput.m_specularColor.rgb;
+ OUT.m_color.a = lightingOutput.m_diffuseColor.a;
+ OUT.m_depth = depth;
+#else
OUT.m_diffuseColor = lightingOutput.m_diffuseColor;
OUT.m_specularColor = lightingOutput.m_specularColor;
OUT.m_specularF0 = lightingOutput.m_specularF0;
OUT.m_albedo = lightingOutput.m_albedo;
OUT.m_normal = lightingOutput.m_normal;
OUT.m_depth = depth;
-
+#endif
return OUT;
}
@@ -335,12 +342,16 @@ ForwardPassOutput StandardPbr_ForwardPassPS_EDS(VSOutput IN, bool isFrontFace :
PbrLightingOutput lightingOutput = ForwardPassPS_Common(IN, isFrontFace, depth);
+#ifdef UNIFIED_FORWARD_OUTPUT
+ OUT.m_color.rgb = lightingOutput.m_diffuseColor.rgb + lightingOutput.m_specularColor.rgb;
+ OUT.m_color.a = lightingOutput.m_diffuseColor.a;
+#else
OUT.m_diffuseColor = lightingOutput.m_diffuseColor;
OUT.m_specularColor = lightingOutput.m_specularColor;
OUT.m_specularF0 = lightingOutput.m_specularF0;
OUT.m_albedo = lightingOutput.m_albedo;
OUT.m_normal = lightingOutput.m_normal;
-
+#endif
return OUT;
}
diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward.azsl
new file mode 100644
index 0000000000..c87faffcbe
--- /dev/null
+++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward.azsl
@@ -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.
+*
+*/
+
+// NOTE: This file is a temporary workaround until .shader files can #define macros for their .azsl files
+
+#define QUALITY_LOW_END 1
+
+#include "StandardPBR_ForwardPass.azsl"
diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward.shader b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward.shader
new file mode 100644
index 0000000000..44139608ca
--- /dev/null
+++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward.shader
@@ -0,0 +1,59 @@
+{
+ // Note: "LowEnd" shaders are for supporting the low end pipeline
+ // These shaders can be safely added to materials without incurring additional runtime draw
+ // items as draw items for shaders are only created if the scene has a pass with a matching
+ // DrawListTag. If your pipeline doesn't have a "lowEndForward" DrawListTag, no draw items
+ // for this shader will be created.
+
+ "Source" : "./StandardPBR_LowEndForward.azsl",
+
+ "DepthStencilState" :
+ {
+ "Depth" :
+ {
+ "Enable" : true,
+ "CompareFunc" : "GreaterEqual"
+ },
+ "Stencil" :
+ {
+ "Enable" : true,
+ "ReadMask" : "0x00",
+ "WriteMask" : "0xFF",
+ "FrontFace" :
+ {
+ "Func" : "Always",
+ "DepthFailOp" : "Keep",
+ "FailOp" : "Keep",
+ "PassOp" : "Replace"
+ },
+ "BackFace" :
+ {
+ "Func" : "Always",
+ "DepthFailOp" : "Keep",
+ "FailOp" : "Keep",
+ "PassOp" : "Replace"
+ }
+ }
+ },
+
+ "CompilerHints" : {
+ "DisableOptimizations" : false
+ },
+
+ "ProgramSettings":
+ {
+ "EntryPoints":
+ [
+ {
+ "name": "StandardPbr_ForwardPassVS",
+ "type": "Vertex"
+ },
+ {
+ "name": "StandardPbr_ForwardPassPS",
+ "type": "Fragment"
+ }
+ ]
+ },
+
+ "DrawList" : "lowEndForward"
+}
diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward_EDS.shader b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward_EDS.shader
new file mode 100644
index 0000000000..9faa1d3698
--- /dev/null
+++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward_EDS.shader
@@ -0,0 +1,59 @@
+{
+ // Note: "LowEnd" shaders are for supporting the low end pipeline
+ // These shaders can be safely added to materials without incurring additional runtime draw
+ // items as draw items for shaders are only created if the scene has a pass with a matching
+ // DrawListTag. If your pipeline doesn't have a "lowEndForward" DrawListTag, no draw items
+ // for this shader will be created.
+
+ "Source" : "./StandardPBR_LowEndForward.azsl",
+
+ "DepthStencilState" :
+ {
+ "Depth" :
+ {
+ "Enable" : true,
+ "CompareFunc" : "GreaterEqual"
+ },
+ "Stencil" :
+ {
+ "Enable" : true,
+ "ReadMask" : "0x00",
+ "WriteMask" : "0xFF",
+ "FrontFace" :
+ {
+ "Func" : "Always",
+ "DepthFailOp" : "Keep",
+ "FailOp" : "Keep",
+ "PassOp" : "Replace"
+ },
+ "BackFace" :
+ {
+ "Func" : "Always",
+ "DepthFailOp" : "Keep",
+ "FailOp" : "Keep",
+ "PassOp" : "Replace"
+ }
+ }
+ },
+
+ "CompilerHints" : {
+ "DisableOptimizations" : false
+ },
+
+ "ProgramSettings":
+ {
+ "EntryPoints":
+ [
+ {
+ "name": "StandardPbr_ForwardPassVS",
+ "type": "Vertex"
+ },
+ {
+ "name": "StandardPbr_ForwardPassPS_EDS",
+ "type": "Fragment"
+ }
+ ]
+ },
+
+ "DrawList" : "lowEndForward"
+}
diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ShaderEnable.lua b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ShaderEnable.lua
index 7c3d989c35..2733713122 100644
--- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ShaderEnable.lua
+++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ShaderEnable.lua
@@ -29,26 +29,33 @@ function Process(context)
local depthPass = context:GetShaderByTag("DepthPass")
local shadowMap = context:GetShaderByTag("Shadowmap")
local forwardPassEDS = context:GetShaderByTag("ForwardPass_EDS")
+ local lowEndForwardEDS = context:GetShaderByTag("LowEndForward_EDS")
+
local depthPassWithPS = context:GetShaderByTag("DepthPass_WithPS")
local shadowMapWitPS = context:GetShaderByTag("Shadowmap_WithPS")
local forwardPass = context:GetShaderByTag("ForwardPass")
+ local lowEndForward = context:GetShaderByTag("LowEndForward")
if parallaxEnabled and parallaxPdoEnabled then
depthPass:SetEnabled(false)
shadowMap:SetEnabled(false)
forwardPassEDS:SetEnabled(false)
+ lowEndForwardEDS:SetEnabled(false)
depthPassWithPS:SetEnabled(true)
shadowMapWitPS:SetEnabled(true)
forwardPass:SetEnabled(true)
+ lowEndForward:SetEnabled(true)
else
depthPass:SetEnabled(opacityMode == OpacityMode_Opaque)
shadowMap:SetEnabled(opacityMode == OpacityMode_Opaque)
forwardPassEDS:SetEnabled((opacityMode == OpacityMode_Opaque) or (opacityMode == OpacityMode_Blended) or (opacityMode == OpacityMode_TintedTransparent))
+ lowEndForwardEDS:SetEnabled((opacityMode == OpacityMode_Opaque) or (opacityMode == OpacityMode_Blended) or (opacityMode == OpacityMode_TintedTransparent))
depthPassWithPS:SetEnabled(opacityMode == OpacityMode_Cutout)
shadowMapWitPS:SetEnabled(opacityMode == OpacityMode_Cutout)
forwardPass:SetEnabled(opacityMode == OpacityMode_Cutout)
+ lowEndForward:SetEnabled(opacityMode == OpacityMode_Cutout)
end
context:GetShaderByTag("DepthPassTransparentMin"):SetEnabled((opacityMode == OpacityMode_Blended) or (opacityMode == OpacityMode_TintedTransparent))
diff --git a/Gems/Atom/Feature/Common/Assets/Passes/Forward.pass b/Gems/Atom/Feature/Common/Assets/Passes/Forward.pass
index 31a8ed1879..b66e3bb4e1 100644
--- a/Gems/Atom/Feature/Common/Assets/Passes/Forward.pass
+++ b/Gems/Atom/Feature/Common/Assets/Passes/Forward.pass
@@ -148,22 +148,6 @@
},
"LoadAction": "Clear"
}
- },
- {
- "Name": "ScatterDistanceOutput",
- "SlotType": "Output",
- "ScopeAttachmentUsage": "RenderTarget",
- "LoadStoreAction": {
- "ClearValue": {
- "Value": [
- 0.0,
- 0.0,
- 0.0,
- 0.0
- ]
- },
- "LoadAction": "Clear"
- }
}
],
"ImageAttachments": [
@@ -238,19 +222,6 @@
"AssetRef": {
"FilePath": "Textures/BRDFTexture.attimage"
}
- },
- {
- "Name": "ScatterDistanceImage",
- "SizeSource": {
- "Source": {
- "Pass": "Parent",
- "Attachment": "SwapChainOutput"
- }
- },
- "ImageDescriptor": {
- "Format": "R11G11B10_FLOAT",
- "SharedQueueMask": "Graphics"
- }
}
],
"Connections": [
@@ -295,13 +266,6 @@
"Pass": "This",
"Attachment": "BRDFTexture"
}
- },
- {
- "LocalSlot": "ScatterDistanceOutput",
- "AttachmentRef": {
- "Pass": "This",
- "Attachment": "ScatterDistanceImage"
- }
}
]
}
diff --git a/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass b/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass
new file mode 100644
index 0000000000..3e804d23e2
--- /dev/null
+++ b/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass
@@ -0,0 +1,146 @@
+{
+ "Type": "JsonSerialization",
+ "Version": 1,
+ "ClassName": "PassAsset",
+ "ClassData": {
+ "PassTemplate": {
+ "Name": "LightAdaptationParentTemplate",
+ "PassClass": "ParentPass",
+ "Slots": [
+ // Inputs...
+ {
+ "Name": "LightingInput",
+ "SlotType": "Input"
+ },
+ // SwapChain here is only used to reference the frame height and format
+ {
+ "Name": "SwapChainOutput",
+ "SlotType": "InputOutput"
+ },
+ // Outputs...
+ {
+ "Name": "Output",
+ "SlotType": "Output"
+ },
+ // Debug Outputs...
+ {
+ "Name": "LuminanceMipChainOutput",
+ "SlotType": "Output"
+ }
+ ],
+ "Connections": [
+ {
+ "LocalSlot": "Output",
+ "AttachmentRef": {
+ "Pass": "DisplayMapperPass",
+ "Attachment": "Output"
+ }
+ },
+ {
+ "LocalSlot": "LuminanceMipChainOutput",
+ "AttachmentRef": {
+ "Pass": "DownsampleLuminanceMipChain",
+ "Attachment": "MipChainInputOutput"
+ }
+ }
+ ],
+ "PassRequests": [
+ {
+ "Name": "DownsampleLuminanceMinAvgMax",
+ "TemplateName": "DownsampleLuminanceMinAvgMaxCS",
+ "Connections": [
+ {
+ "LocalSlot": "Input",
+ "AttachmentRef": {
+ "Pass": "Parent",
+ "Attachment": "LightingInput"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "DownsampleLuminanceMipChain",
+ "TemplateName": "DownsampleMipChainTemplate",
+ "Connections": [
+ {
+ "LocalSlot": "MipChainInputOutput",
+ "AttachmentRef": {
+ "Pass": "DownsampleLuminanceMinAvgMax",
+ "Attachment": "Output"
+ }
+ }
+ ],
+ "PassData": {
+ "$type": "DownsampleMipChainPassData",
+ "ShaderAsset": {
+ "FilePath": "Shaders/PostProcessing/DownsampleMinAvgMaxCS.shader"
+ }
+ }
+ },
+ {
+ "Name": "EyeAdaptationPass",
+ "TemplateName": "EyeAdaptationTemplate",
+ "Enabled": false,
+ "Connections": [
+ {
+ "LocalSlot": "SceneLuminanceInput",
+ "AttachmentRef": {
+ "Pass": "DownsampleLuminanceMipChain",
+ "Attachment": "MipChainInputOutput"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "LookModificationTransformPass",
+ "TemplateName": "LookModificationTransformTemplate",
+ "Enabled": true,
+ "Connections": [
+ {
+ "LocalSlot": "Input",
+ "AttachmentRef": {
+ "Pass": "Parent",
+ "Attachment": "LightingInput"
+ }
+ },
+ {
+ "LocalSlot": "EyeAdaptationDataInput",
+ "AttachmentRef": {
+ "Pass": "EyeAdaptationPass",
+ "Attachment": "EyeAdaptationDataInputOutput"
+ }
+ },
+ {
+ "LocalSlot": "SwapChainOutput",
+ "AttachmentRef": {
+ "Pass": "Parent",
+ "Attachment": "SwapChainOutput"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "DisplayMapperPass",
+ "TemplateName": "DisplayMapperTemplate",
+ "Enabled": true,
+ "Connections": [
+ {
+ "LocalSlot": "Input",
+ "AttachmentRef": {
+ "Pass": "LookModificationTransformPass",
+ "Attachment": "Output"
+ }
+ },
+ {
+ "LocalSlot": "SwapChainOutput",
+ "AttachmentRef": {
+ "Pass": "Parent",
+ "Attachment": "SwapChainOutput"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
+}
diff --git a/Gems/Atom/Feature/Common/Assets/Passes/LowEndForward.pass b/Gems/Atom/Feature/Common/Assets/Passes/LowEndForward.pass
new file mode 100644
index 0000000000..4b865fcb6d
--- /dev/null
+++ b/Gems/Atom/Feature/Common/Assets/Passes/LowEndForward.pass
@@ -0,0 +1,133 @@
+{
+ "Type": "JsonSerialization",
+ "Version": 1,
+ "ClassName": "PassAsset",
+ "ClassData": {
+ "PassTemplate": {
+ "Name": "LowEndForwardPassTemplate",
+ "PassClass": "RasterPass",
+ "Slots": [
+ // Inputs...
+ {
+ "Name": "BRDFTextureInput",
+ "ShaderInputName": "m_brdfMap",
+ "SlotType": "Input",
+ "ScopeAttachmentUsage": "Shader"
+ },
+ {
+ "Name": "DirectionalLightShadowmap",
+ "ShaderInputName": "m_directionalLightShadowmap",
+ "SlotType": "Input",
+ "ScopeAttachmentUsage": "Shader",
+ "ImageViewDesc": {
+ "IsArray": 1
+ }
+ },
+ {
+ "Name": "ExponentialShadowmapDirectional",
+ "ShaderInputName": "m_directionalLightExponentialShadowmap",
+ "SlotType": "Input",
+ "ScopeAttachmentUsage": "Shader",
+ "ImageViewDesc": {
+ "IsArray": 1
+ }
+ },
+ {
+ "Name": "ProjectedShadowmap",
+ "ShaderInputName": "m_projectedShadowmaps",
+ "SlotType": "Input",
+ "ScopeAttachmentUsage": "Shader",
+ "ImageViewDesc": {
+ "IsArray": 1
+ }
+ },
+ {
+ "Name": "ExponentialShadowmapProjected",
+ "ShaderInputName": "m_projectedExponentialShadowmap",
+ "SlotType": "Input",
+ "ScopeAttachmentUsage": "Shader",
+ "ImageViewDesc": {
+ "IsArray": 1
+ }
+ },
+ {
+ "Name": "TileLightData",
+ "SlotType": "Input",
+ "ShaderInputName": "m_tileLightData",
+ "ScopeAttachmentUsage": "Shader"
+ },
+ {
+ "Name": "LightListRemapped",
+ "SlotType": "Input",
+ "ShaderInputName": "m_lightListRemapped",
+ "ScopeAttachmentUsage": "Shader"
+ },
+ // Input/Outputs...
+ {
+ "Name": "DepthStencilInputOutput",
+ "SlotType": "InputOutput",
+ "ScopeAttachmentUsage": "DepthStencil"
+ },
+ // Outputs...
+ {
+ "Name": "LightingOutput",
+ "SlotType": "Output",
+ "ScopeAttachmentUsage": "RenderTarget",
+ "LoadStoreAction": {
+ "ClearValue": {
+ "Value": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ]
+ },
+ "LoadAction": "Clear"
+ }
+ }
+ ],
+ "ImageAttachments": [
+ {
+ "Name": "LightingAttachment",
+ "SizeSource": {
+ "Source": {
+ "Pass": "Parent",
+ "Attachment": "SwapChainOutput"
+ }
+ },
+ "MultisampleSource": {
+ "Pass": "This",
+ "Attachment": "DepthStencilInputOutput"
+ },
+ "ImageDescriptor": {
+ "Format": "R16G16B16A16_FLOAT",
+ "SharedQueueMask": "Graphics"
+ }
+ },
+ {
+ "Name": "BRDFTexture",
+ "Lifetime": "Imported",
+ "AssetRef": {
+ "FilePath": "Textures/BRDFTexture.attimage"
+ }
+ }
+ ],
+ "Connections": [
+ {
+ "LocalSlot": "LightingOutput",
+ "AttachmentRef": {
+ "Pass": "This",
+ "Attachment": "LightingAttachment"
+ }
+ },
+ {
+ "LocalSlot": "BRDFTextureInput",
+ "AttachmentRef": {
+ "Pass": "This",
+ "Attachment": "BRDFTexture"
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/Gems/Atom/Feature/Common/Assets/Passes/LowEndPipeline.pass b/Gems/Atom/Feature/Common/Assets/Passes/LowEndPipeline.pass
new file mode 100644
index 0000000000..b19569fb9d
--- /dev/null
+++ b/Gems/Atom/Feature/Common/Assets/Passes/LowEndPipeline.pass
@@ -0,0 +1,344 @@
+{
+ "Type": "JsonSerialization",
+ "Version": 1,
+ "ClassName": "PassAsset",
+ "ClassData": {
+ "PassTemplate": {
+ "Name": "LowEndPipelineTemplate",
+ "PassClass": "ParentPass",
+ "Slots": [
+ {
+ "Name": "SwapChainOutput",
+ "SlotType": "InputOutput",
+ "ScopeAttachmentUsage": "RenderTarget"
+ }
+ ],
+ "PassRequests": [
+ {
+ "Name": "MorphTargetPass",
+ "TemplateName": "MorphTargetPassTemplate"
+ },
+ {
+ "Name": "SkinningPass",
+ "TemplateName": "SkinningPassTemplate",
+ "Connections": [
+ {
+ "LocalSlot": "SkinnedMeshOutputStream",
+ "AttachmentRef": {
+ "Pass": "MorphTargetPass",
+ "Attachment": "MorphTargetDeltaOutput"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "DepthPrePass",
+ "TemplateName": "DepthMSAAParentTemplate",
+ "Connections": [
+ {
+ "LocalSlot": "SkinnedMeshes",
+ "AttachmentRef": {
+ "Pass": "SkinningPass",
+ "Attachment": "SkinnedMeshOutputStream"
+ }
+ },
+ {
+ "LocalSlot": "SwapChainOutput",
+ "AttachmentRef": {
+ "Pass": "Parent",
+ "Attachment": "SwapChainOutput"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "LightCullingPass",
+ "TemplateName": "LightCullingParentTemplate",
+ "Connections": [
+ {
+ "LocalSlot": "SkinnedMeshes",
+ "AttachmentRef": {
+ "Pass": "SkinningPass",
+ "Attachment": "SkinnedMeshOutputStream"
+ }
+ },
+ {
+ "LocalSlot": "DepthMSAA",
+ "AttachmentRef": {
+ "Pass": "DepthPrePass",
+ "Attachment": "DepthMSAA"
+ }
+ },
+ {
+ "LocalSlot": "SwapChainOutput",
+ "AttachmentRef": {
+ "Pass": "Parent",
+ "Attachment": "SwapChainOutput"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "ShadowPass",
+ "TemplateName": "ShadowParentTemplate",
+ "Connections": [
+ {
+ "LocalSlot": "SkinnedMeshes",
+ "AttachmentRef": {
+ "Pass": "SkinningPass",
+ "Attachment": "SkinnedMeshOutputStream"
+ }
+ },
+ {
+ "LocalSlot": "SwapChainOutput",
+ "AttachmentRef": {
+ "Pass": "Parent",
+ "Attachment": "SwapChainOutput"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "ForwardPass",
+ "TemplateName": "LowEndForwardPassTemplate",
+ "Connections": [
+ // Inputs...
+ {
+ "LocalSlot": "DirectionalLightShadowmap",
+ "AttachmentRef": {
+ "Pass": "ShadowPass",
+ "Attachment": "DirectionalShadowmap"
+ }
+ },
+ {
+ "LocalSlot": "ExponentialShadowmapDirectional",
+ "AttachmentRef": {
+ "Pass": "ShadowPass",
+ "Attachment": "DirectionalESM"
+ }
+ },
+ {
+ "LocalSlot": "ProjectedShadowmap",
+ "AttachmentRef": {
+ "Pass": "ShadowPass",
+ "Attachment": "ProjectedShadowmap"
+ }
+ },
+ {
+ "LocalSlot": "ExponentialShadowmapProjected",
+ "AttachmentRef": {
+ "Pass": "ShadowPass",
+ "Attachment": "ProjectedESM"
+ }
+ },
+ {
+ "LocalSlot": "TileLightData",
+ "AttachmentRef": {
+ "Pass": "LightCullingPass",
+ "Attachment": "TileLightData"
+ }
+ },
+ {
+ "LocalSlot": "LightListRemapped",
+ "AttachmentRef": {
+ "Pass": "LightCullingPass",
+ "Attachment": "LightListRemapped"
+ }
+ },
+ // Input/Outputs...
+ {
+ "LocalSlot": "DepthStencilInputOutput",
+ "AttachmentRef": {
+ "Pass": "DepthPrePass",
+ "Attachment": "DepthMSAA"
+ }
+ }
+ ],
+ "PassData": {
+ "$type": "RasterPassData",
+ "DrawListTag": "lowEndForward",
+ "PipelineViewTag": "MainCamera",
+ "PassSrgAsset": {
+ "FilePath": "shaderlib/atom/features/pbr/forwardpasssrg.azsli:PassSrg"
+ }
+ }
+ },
+ {
+ "Name": "SkyBoxPass",
+ "TemplateName": "SkyBoxTemplate",
+ "Enabled": true,
+ "Connections": [
+ {
+ "LocalSlot": "SpecularInputOutput",
+ "AttachmentRef": {
+ "Pass": "ForwardPass",
+ "Attachment": "LightingOutput"
+ }
+ },
+ {
+ "LocalSlot": "SkyBoxDepth",
+ "AttachmentRef": {
+ "Pass": "ForwardPass",
+ "Attachment": "DepthStencilInputOutput"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "MSAAResolvePass",
+ "TemplateName": "MSAAResolveColorTemplate",
+ "Connections": [
+ {
+ "LocalSlot": "Input",
+ "AttachmentRef": {
+ "Pass": "SkyBoxPass",
+ "Attachment": "SpecularInputOutput"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "TransparentPass",
+ "TemplateName": "TransparentParentTemplate",
+ "Connections": [
+ {
+ "LocalSlot": "DirectionalShadowmap",
+ "AttachmentRef": {
+ "Pass": "ShadowPass",
+ "Attachment": "DirectionalShadowmap"
+ }
+ },
+ {
+ "LocalSlot": "DirectionalESM",
+ "AttachmentRef": {
+ "Pass": "ShadowPass",
+ "Attachment": "DirectionalESM"
+ }
+ },
+ {
+ "LocalSlot": "ProjectedShadowmap",
+ "AttachmentRef": {
+ "Pass": "ShadowPass",
+ "Attachment": "ProjectedShadowmap"
+ }
+ },
+ {
+ "LocalSlot": "ProjectedESM",
+ "AttachmentRef": {
+ "Pass": "ShadowPass",
+ "Attachment": "ProjectedESM"
+ }
+ },
+ {
+ "LocalSlot": "TileLightData",
+ "AttachmentRef": {
+ "Pass": "LightCullingPass",
+ "Attachment": "TileLightData"
+ }
+ },
+ {
+ "LocalSlot": "LightListRemapped",
+ "AttachmentRef": {
+ "Pass": "LightCullingPass",
+ "Attachment": "LightListRemapped"
+ }
+ },
+ {
+ "LocalSlot": "DepthStencil",
+ "AttachmentRef": {
+ "Pass": "DepthPrePass",
+ "Attachment": "Depth"
+ }
+ },
+ {
+ "LocalSlot": "InputOutput",
+ "AttachmentRef": {
+ "Pass": "MSAAResolvePass",
+ "Attachment": "Output"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "LightAdaptation",
+ "TemplateName": "LightAdaptationParentTemplate",
+ "Connections": [
+ {
+ "LocalSlot": "LightingInput",
+ "AttachmentRef": {
+ "Pass": "TransparentPass",
+ "Attachment": "InputOutput"
+ }
+ },
+ {
+ "LocalSlot": "SwapChainOutput",
+ "AttachmentRef": {
+ "Pass": "Parent",
+ "Attachment": "SwapChainOutput"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "AuxGeomPass",
+ "TemplateName": "AuxGeomPassTemplate",
+ "Enabled": true,
+ "Connections": [
+ {
+ "LocalSlot": "ColorInputOutput",
+ "AttachmentRef": {
+ "Pass": "LightAdaptation",
+ "Attachment": "Output"
+ }
+ },
+ {
+ "LocalSlot": "DepthInputOutput",
+ "AttachmentRef": {
+ "Pass": "DepthPrePass",
+ "Attachment": "Depth"
+ }
+ }
+ ],
+ "PassData": {
+ "$type": "RasterPassData",
+ "DrawListTag": "auxgeom",
+ "PipelineViewTag": "MainCamera"
+ }
+ },
+ {
+ "Name": "UIPass",
+ "TemplateName": "UIParentTemplate",
+ "Connections": [
+ {
+ "LocalSlot": "InputOutput",
+ "AttachmentRef": {
+ "Pass": "AuxGeomPass",
+ "Attachment": "ColorInputOutput"
+ }
+ }
+ ]
+ },
+ {
+ "Name": "CopyToSwapChain",
+ "TemplateName": "FullscreenCopyTemplate",
+ "Connections": [
+ {
+ "LocalSlot": "Input",
+ "AttachmentRef": {
+ "Pass": "UIPass",
+ "Attachment": "InputOutput"
+ }
+ },
+ {
+ "LocalSlot": "Output",
+ "AttachmentRef": {
+ "Pass": "Parent",
+ "Attachment": "SwapChainOutput"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
+}
diff --git a/Gems/Atom/Feature/Common/Assets/Passes/OpaqueParent.pass b/Gems/Atom/Feature/Common/Assets/Passes/OpaqueParent.pass
index dda120e164..a691fe2534 100644
--- a/Gems/Atom/Feature/Common/Assets/Passes/OpaqueParent.pass
+++ b/Gems/Atom/Feature/Common/Assets/Passes/OpaqueParent.pass
@@ -305,7 +305,7 @@
},
{
"Name": "SkyBoxPass",
- "TemplateName": "SkyBoxTemplate",
+ "TemplateName": "SkyBoxTwoOutputsTemplate",
"Enabled": true,
"Connections": [
{
diff --git a/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset b/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset
index b83ab65ff2..c56e8932b1 100644
--- a/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset
+++ b/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset
@@ -92,6 +92,10 @@
"Name": "SkyBoxTemplate",
"Path": "Passes/SkyBox.pass"
},
+ {
+ "Name": "SkyBoxTwoOutputsTemplate",
+ "Path": "Passes/SkyBox_TwoOutputs.pass"
+ },
{
"Name": "UIPassTemplate",
"Path": "Passes/UI.pass"
@@ -483,6 +487,18 @@
{
"Name": "UIParentTemplate",
"Path": "Passes/UIParent.pass"
+ },
+ {
+ "Name": "LightAdaptationParentTemplate",
+ "Path": "Passes/LightAdaptationParent.pass"
+ },
+ {
+ "Name": "LowEndForwardPassTemplate",
+ "Path": "Passes/LowEndForward.pass"
+ },
+ {
+ "Name": "LowEndPipelineTemplate",
+ "Path": "Passes/LowEndPipeline.pass"
}
]
}
diff --git a/Gems/Atom/Feature/Common/Assets/Passes/PostProcessParent.pass b/Gems/Atom/Feature/Common/Assets/Passes/PostProcessParent.pass
index 37b1ee5c5a..36f7f1e985 100644
--- a/Gems/Atom/Feature/Common/Assets/Passes/PostProcessParent.pass
+++ b/Gems/Atom/Feature/Common/Assets/Passes/PostProcessParent.pass
@@ -40,7 +40,7 @@
{
"LocalSlot": "Output",
"AttachmentRef": {
- "Pass": "DisplayMapperPass",
+ "Pass": "LightAdaptation",
"Attachment": "Output"
}
},
@@ -54,8 +54,8 @@
{
"LocalSlot": "LuminanceMipChainOutput",
"AttachmentRef": {
- "Pass": "DownsampleLuminanceMipChain",
- "Attachment": "MipChainInputOutput"
+ "Pass": "LightAdaptation",
+ "Attachment": "LuminanceMipChainOutput"
}
}
],
@@ -115,94 +115,16 @@
}
]
},
- // Everything before this point deals in raw lighting values
- // ---------------------------------------------------------
- // Everything after starts to map to values we see on screen
{
- "Name": "DownsampleLuminanceMinAvgMax",
- "TemplateName": "DownsampleLuminanceMinAvgMaxCS",
+ "Name": "LightAdaptation",
+ "TemplateName": "LightAdaptationParentTemplate",
"Connections": [
{
- "LocalSlot": "Input",
+ "LocalSlot": "LightingInput",
"AttachmentRef": {
"Pass": "BloomPass",
"Attachment": "InputOutput"
}
- }
- ]
- },
- {
- "Name": "DownsampleLuminanceMipChain",
- "TemplateName": "DownsampleMipChainTemplate",
- "Connections": [
- {
- "LocalSlot": "MipChainInputOutput",
- "AttachmentRef": {
- "Pass": "DownsampleLuminanceMinAvgMax",
- "Attachment": "Output"
- }
- }
- ],
- "PassData": {
- "$type": "DownsampleMipChainPassData",
- "ShaderAsset": {
- "FilePath": "Shaders/PostProcessing/DownsampleMinAvgMaxCS.shader"
- }
- }
- },
- {
- "Name": "EyeAdaptationPass",
- "TemplateName": "EyeAdaptationTemplate",
- "Enabled": false,
- "Connections": [
- {
- "LocalSlot": "SceneLuminanceInput",
- "AttachmentRef": {
- "Pass": "DownsampleLuminanceMipChain",
- "Attachment": "MipChainInputOutput"
- }
- }
- ]
- },
- {
- "Name": "LookModificationTransformPass",
- "TemplateName": "LookModificationTransformTemplate",
- "Enabled": true,
- "Connections": [
- {
- "LocalSlot": "Input",
- "AttachmentRef": {
- "Pass": "BloomPass",
- "Attachment": "InputOutput"
- }
- },
- {
- "LocalSlot": "EyeAdaptationDataInput",
- "AttachmentRef": {
- "Pass": "EyeAdaptationPass",
- "Attachment": "EyeAdaptationDataInputOutput"
- }
- },
- {
- "LocalSlot": "SwapChainOutput",
- "AttachmentRef": {
- "Pass": "Parent",
- "Attachment": "SwapChainOutput"
- }
- }
- ]
- },
- {
- "Name": "DisplayMapperPass",
- "TemplateName": "DisplayMapperTemplate",
- "Enabled": true,
- "Connections": [
- {
- "LocalSlot": "Input",
- "AttachmentRef": {
- "Pass": "LookModificationTransformPass",
- "Attachment": "Output"
- }
},
{
"LocalSlot": "SwapChainOutput",
diff --git a/Gems/Atom/Feature/Common/Assets/Passes/SkyBox.pass b/Gems/Atom/Feature/Common/Assets/Passes/SkyBox.pass
index 57f442e5de..fb16271ba7 100644
--- a/Gems/Atom/Feature/Common/Assets/Passes/SkyBox.pass
+++ b/Gems/Atom/Feature/Common/Assets/Passes/SkyBox.pass
@@ -12,11 +12,6 @@
"SlotType": "InputOutput",
"ScopeAttachmentUsage": "RenderTarget"
},
- {
- "Name": "ReflectionInputOutput",
- "SlotType": "InputOutput",
- "ScopeAttachmentUsage": "RenderTarget"
- },
{
"Name": "SkyBoxDepth",
"SlotType": "InputOutput",
diff --git a/Gems/Atom/Feature/Common/Assets/Passes/SkyBox_TwoOutputs.pass b/Gems/Atom/Feature/Common/Assets/Passes/SkyBox_TwoOutputs.pass
new file mode 100644
index 0000000000..0ed7b39288
--- /dev/null
+++ b/Gems/Atom/Feature/Common/Assets/Passes/SkyBox_TwoOutputs.pass
@@ -0,0 +1,43 @@
+{
+ "Type": "JsonSerialization",
+ "Version": 1,
+ "ClassName": "PassAsset",
+ "ClassData": {
+ "PassTemplate": {
+ "Name": "SkyBoxTwoOutputsTemplate",
+ "PassClass": "FullScreenTriangle",
+ "Slots": [
+ {
+ "Name": "SpecularInputOutput",
+ "SlotType": "InputOutput",
+ "ScopeAttachmentUsage": "RenderTarget"
+ },
+ {
+ "Name": "ReflectionInputOutput",
+ "SlotType": "InputOutput",
+ "ScopeAttachmentUsage": "RenderTarget"
+ },
+ {
+ "Name": "SkyBoxDepth",
+ "SlotType": "InputOutput",
+ "ScopeAttachmentUsage": "DepthStencil"
+ }
+ ],
+ "PassData": {
+ "$type": "FullscreenTrianglePassData",
+ "ShaderAsset": {
+ "FilePath": "shaders/skybox/skybox_twooutputs.shader"
+ },
+ "PipelineViewTag": "MainCamera",
+ "ShaderDataMappings": {
+ "FloatMappings": [
+ {
+ "Name": "m_sunIntensityMultiplier",
+ "Value": 1.0
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/DefaultObjectSrg.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/DefaultObjectSrg.azsli
index 50896cdf25..abc4ec7fc4 100644
--- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/DefaultObjectSrg.azsli
+++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/DefaultObjectSrg.azsli
@@ -31,6 +31,16 @@ ShaderResourceGroup ObjectSrg : SRG_PerObject
return SceneSrg::GetObjectToWorldInverseTransposeMatrix(m_objectId);
}
+ //[GFX TODO][ATOM-15280] Move wrinkle mask data from the default object srg into something specific to the Skin shader
+ uint m_wrinkle_mask_count;
+ float4 m_wrinkle_mask_weights[4];
+ Texture2D m_wrinkle_masks[16];
+
+ float GetWrinkleMaskWeight(uint index)
+ {
+ return m_wrinkle_mask_weights[index / 4][index % 4];
+ }
+
//! Reflection Probe (smallest probe volume that overlaps the object position)
struct ReflectionProbeData
{
diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/ForwardPassOutput.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/ForwardPassOutput.azsli
index acc215f1c9..5821deb3b1 100644
--- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/ForwardPassOutput.azsli
+++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/ForwardPassOutput.azsli
@@ -10,6 +10,21 @@
*
*/
+#ifdef UNIFIED_FORWARD_OUTPUT
+
+struct ForwardPassOutput
+{
+ float4 m_color : SV_Target0;
+};
+
+struct ForwardPassOutputWithDepth
+{
+ float4 m_color : SV_Target0;
+ float m_depth : SV_Depth;
+};
+
+#else
+
struct ForwardPassOutput
{
float4 m_diffuseColor : SV_Target0; //!< RGB = Diffuse Lighting, A = Blend Alpha (for blended surfaces) OR A = special encoding of surfaceScatteringFactor, m_subsurfaceScatteringQuality, o_enableSubsurfaceScattering
@@ -30,3 +45,5 @@ struct ForwardPassOutputWithDepth
float4 m_normal : SV_Target4;
float m_depth : SV_Depth;
};
+
+#endif
diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/Ibl.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/Ibl.azsli
index 7400005508..3be9d5756a 100644
--- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/Ibl.azsli
+++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/Ibl.azsli
@@ -12,38 +12,39 @@
#pragma once
+// --- Static Options Available ---
+// FORCE_IBL_IN_FORWARD_PASS - forces IBL lighting to be run in the forward pass, used in pipelines that don't have a reflection pass
+
#include
#include
#include
#include
-void ApplyIblDiffuse(
+float3 GetIblDiffuse(
float3 normal,
float3 albedo,
- float3 diffuseResponse,
- out float3 outDiffuse)
+ float3 diffuseResponse)
{
float3 irradianceDir = MultiplyVectorQuaternion(normal, SceneSrg::m_iblOrientation);
float3 diffuseSample = SceneSrg::m_diffuseEnvMap.Sample(SceneSrg::m_samplerEnv, GetCubemapCoords(irradianceDir)).rgb;
- outDiffuse = diffuseResponse * albedo * diffuseSample;
+ return diffuseResponse * albedo * diffuseSample;
}
-void ApplyIblSpecular(
+float3 GetIblSpecular(
float3 position,
float3 normal,
float3 specularF0,
float roughnessLinear,
float3 dirToCamera,
- float2 brdf,
- out float3 outSpecular)
+ float2 brdf)
{
float3 reflectDir = reflect(-dirToCamera, normal);
reflectDir = MultiplyVectorQuaternion(reflectDir, SceneSrg::m_iblOrientation);
// global
- outSpecular = SceneSrg::m_specularEnvMap.SampleLevel(SceneSrg::m_samplerEnv, GetCubemapCoords(reflectDir), GetRoughnessMip(roughnessLinear)).rgb;
+ float3 outSpecular = SceneSrg::m_specularEnvMap.SampleLevel(SceneSrg::m_samplerEnv, GetCubemapCoords(reflectDir), GetRoughnessMip(roughnessLinear)).rgb;
outSpecular *= (specularF0 * brdf.x + brdf.y);
// reflection probe
@@ -72,86 +73,55 @@ void ApplyIblSpecular(
outSpecular = lerp(outSpecular, probeSpecular, blendAmount);
}
+ return outSpecular;
}
void ApplyIBL(Surface surface, inout LightingData lightingData)
{
- if (o_opacity_mode == OpacityMode::Blended || o_opacity_mode == OpacityMode::TintedTransparent)
+#ifdef FORCE_IBL_IN_FORWARD_PASS
+ bool useDiffuseIbl = true;
+ bool useSpecularIbl = true;
+ bool useIbl = o_enableIBL;
+#else
+ bool isTransparent = (o_opacity_mode == OpacityMode::Blended || o_opacity_mode == OpacityMode::TintedTransparent);
+ bool useDiffuseIbl = isTransparent;
+ bool useSpecularIbl = (isTransparent || o_meshUseForwardPassIBLSpecular || o_materialUseForwardPassIBLSpecular);
+ bool useIbl = o_enableIBL && (useDiffuseIbl || useSpecularIbl);
+#endif
+
+ if(useIbl)
{
- // transparencies currently require IBL in the forward pass
- if (o_enableIBL)
+ float iblExposureFactor = pow(2.0, SceneSrg::m_iblExposure);
+
+ if(useDiffuseIbl)
{
- float3 iblDiffuse = 0.0f;
- ApplyIblDiffuse(
- surface.normal,
- surface.albedo,
- lightingData.diffuseResponse,
- iblDiffuse);
-
- float3 iblSpecular = 0.0f;
- ApplyIblSpecular(
- surface.position,
- surface.normal,
- surface.specularF0,
- surface.roughnessLinear,
- lightingData.dirToCamera,
- lightingData.brdf,
- iblSpecular);
-
- // Adjust IBL lighting by exposure.
- float iblExposureFactor = pow(2.0, SceneSrg::m_iblExposure);
+ float3 iblDiffuse = GetIblDiffuse(surface.normal, surface.albedo, lightingData.diffuseResponse);
lightingData.diffuseLighting += (iblDiffuse * iblExposureFactor * lightingData.diffuseAmbientOcclusion);
- lightingData.specularLighting += (iblSpecular * iblExposureFactor);
}
- }
- else if (o_meshUseForwardPassIBLSpecular || o_materialUseForwardPassIBLSpecular)
- {
- if (o_enableIBL)
- {
- float3 iblSpecular = 0.0f;
- ApplyIblSpecular(
- surface.position,
- surface.normal,
- surface.specularF0,
- surface.roughnessLinear,
- lightingData.dirToCamera,
- lightingData.brdf,
- iblSpecular);
+ if(useSpecularIbl)
+ {
+ float3 iblSpecular = GetIblSpecular(surface.position, surface.normal, surface.specularF0, surface.roughnessLinear, lightingData.dirToCamera, lightingData.brdf);
iblSpecular *= lightingData.multiScatterCompensation;
- if (o_clearCoat_feature_enabled)
+ if (o_clearCoat_feature_enabled && surface.clearCoat.factor > 0.0f)
{
- if (surface.clearCoat.factor > 0.0f)
- {
- float clearCoatNdotV = saturate(dot(surface.clearCoat.normal, lightingData.dirToCamera));
- clearCoatNdotV = max(clearCoatNdotV, 0.01f); // [GFX TODO][ATOM-4466] This is a current band-aid for specular noise at grazing angles.
- float2 clearCoatBrdf = PassSrg::m_brdfMap.Sample(PassSrg::LinearSampler, GetBRDFTexCoords(surface.clearCoat.roughness, clearCoatNdotV)).rg;
+ float clearCoatNdotV = saturate(dot(surface.clearCoat.normal, lightingData.dirToCamera));
+ clearCoatNdotV = max(clearCoatNdotV, 0.01f); // [GFX TODO][ATOM-4466] This is a current band-aid for specular noise at grazing angles.
+ float2 clearCoatBrdf = PassSrg::m_brdfMap.Sample(PassSrg::LinearSampler, GetBRDFTexCoords(surface.clearCoat.roughness, clearCoatNdotV)).rg;
- // clear coat uses fixed IOR = 1.5 represents polyurethane which is the most common material for gloss clear coat
- // coat layer assumed to be dielectric thus don't need multiple scattering compensation
- float3 clearCoatSpecularF0 = float3(0.04f, 0.04f, 0.04f);
- float3 clearCoatIblSpecular = 0.0f;
+ // clear coat uses fixed IOR = 1.5 represents polyurethane which is the most common material for gloss clear coat
+ // coat layer assumed to be dielectric thus don't need multiple scattering compensation
+ float3 clearCoatSpecularF0 = float3(0.04f, 0.04f, 0.04f);
+ float3 clearCoatIblSpecular = GetIblSpecular(surface.position, surface.clearCoat.normal, clearCoatSpecularF0, surface.clearCoat.roughness, lightingData.dirToCamera, clearCoatBrdf);
- ApplyIblSpecular(
- surface.position,
- surface.clearCoat.normal,
- clearCoatSpecularF0,
- surface.clearCoat.roughness,
- lightingData.dirToCamera,
- clearCoatBrdf,
- clearCoatIblSpecular);
-
- clearCoatIblSpecular *= surface.clearCoat.factor;
+ clearCoatIblSpecular *= surface.clearCoat.factor;
- // attenuate base layer energy
- float3 clearCoatResponse = FresnelSchlickWithRoughness(clearCoatNdotV, clearCoatSpecularF0, surface.clearCoat.roughness) * surface.clearCoat.factor;
- iblSpecular = iblSpecular * (1.0 - clearCoatResponse) * (1.0 - clearCoatResponse) + clearCoatIblSpecular;
- }
+ // attenuate base layer energy
+ float3 clearCoatResponse = FresnelSchlickWithRoughness(clearCoatNdotV, clearCoatSpecularF0, surface.clearCoat.roughness) * surface.clearCoat.factor;
+ iblSpecular = iblSpecular * (1.0 - clearCoatResponse) * (1.0 - clearCoatResponse) + clearCoatIblSpecular;
}
-
- float iblExposureFactor = pow(2.0f, SceneSrg::m_iblExposure);
lightingData.specularLighting += (iblSpecular * iblExposureFactor);
}
}
diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/ShaderQualityOptions.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/ShaderQualityOptions.azsli
new file mode 100644
index 0000000000..cc4aa7cf42
--- /dev/null
+++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/ShaderQualityOptions.azsli
@@ -0,0 +1,26 @@
+/*
+* 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.
+*
+*/
+
+#pragma once
+
+// This file translates quality option macros like QUALITY_LOW_END to their relevant settings
+
+#ifdef QUALITY_LOW_END
+
+ // Unifies the forward output into a single lighting buffer instead of splitting it into a GBuffer
+ #define UNIFIED_FORWARD_OUTPUT 1
+
+ // Forces IBL lighting to be executed in the forward pass instead of subsequent refleciton passes
+ #define FORCE_IBL_IN_FORWARD_PASS 1
+
+#endif
+
diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox.azsl
index 1ee30a4f98..1bebb2ec47 100644
--- a/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox.azsl
+++ b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox.azsl
@@ -10,6 +10,9 @@
*
*/
+// --- Static Options Available ---
+// SKYBOX_TWO_OUTPUTS - Skybox renders to two rendertargets instead of one (SkyBox_TwoOutputs.pass writes to specular and reflection targets)
+
#include
#include
#include
@@ -102,7 +105,9 @@ float3 GetCubemapCoords(float3 original)
struct PSOutput
{
float4 m_specular : SV_Target0;
+#ifdef SKYBOX_TWO_OUTPUTS
float4 m_reflection : SV_Target1;
+#endif
};
PSOutput MainPS(VSOutput input)
@@ -163,6 +168,8 @@ PSOutput MainPS(VSOutput input)
PSOutput OUT;
OUT.m_specular = float4(color, 1.0);
+#ifdef SKYBOX_TWO_OUTPUTS
OUT.m_reflection = float4(color, 1.0);
+#endif
return OUT;
}
diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.azsl
new file mode 100644
index 0000000000..feacd2f44f
--- /dev/null
+++ b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.azsl
@@ -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.
+*
+*/
+
+// NOTE: This file is a temporary workaround until .shader files can #define macros for their .azsl files
+
+#define SKYBOX_TWO_OUTPUTS
+
+#include "SkyBox.azsl"
diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.shader b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.shader
new file mode 100644
index 0000000000..ec80d4a20e
--- /dev/null
+++ b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.shader
@@ -0,0 +1,22 @@
+{
+ "Source" : "SkyBox_TwoOutputs",
+
+ "DepthStencilState" : {
+ "Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" }
+ },
+
+ "ProgramSettings":
+ {
+ "EntryPoints":
+ [
+ {
+ "name": "MainVS",
+ "type": "Vertex"
+ },
+ {
+ "name": "MainPS",
+ "type": "Fragment"
+ }
+ ]
+ }
+}
diff --git a/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake b/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake
index 84ef494216..f1d8fa81be 100644
--- a/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake
+++ b/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake
@@ -38,6 +38,7 @@ set(FILES
Materials/Types/StandardMultilayerPBR_ForwardPass_EDS.shader
Materials/Types/StandardMultilayerPBR_Parallax.lua
Materials/Types/StandardMultilayerPBR_ParallaxPerLayer.lua
+ Materials/Types/StandardMultilayerPBR_ShaderEnable.lua
Materials/Types/StandardMultilayerPBR_Shadowmap_WithPS.azsl
Materials/Types/StandardMultilayerPBR_Shadowmap_WithPS.shader
Materials/Types/StandardPBR.materialtype
@@ -52,6 +53,9 @@ set(FILES
Materials/Types/StandardPBR_ForwardPass_EDS.shader
Materials/Types/StandardPBR_HandleOpacityDoubleSided.lua
Materials/Types/StandardPBR_HandleOpacityMode.lua
+ Materials/Types/StandardPBR_LowEndForward.azsl
+ Materials/Types/StandardPBR_LowEndForward.shader
+ Materials/Types/StandardPBR_LowEndForward_EDS.shader
Materials/Types/StandardPBR_ParallaxState.lua
Materials/Types/StandardPBR_Roughness.lua
Materials/Types/StandardPBR_ShaderEnable.lua
@@ -116,6 +120,7 @@ set(FILES
Passes/DiffuseProbeGridBlendDistance.pass
Passes/DiffuseProbeGridBlendIrradiance.pass
Passes/DiffuseProbeGridBorderUpdate.pass
+ Passes/DiffuseProbeGridClassification.pass
Passes/DiffuseProbeGridDownsample.pass
Passes/DiffuseProbeGridRayTracing.pass
Passes/DiffuseProbeGridRelocation.pass
@@ -144,6 +149,7 @@ set(FILES
Passes/FullscreenCopy.pass
Passes/FullscreenOutputOnly.pass
Passes/ImGui.pass
+ Passes/LightAdaptationParent.pass
Passes/LightCulling.pass
Passes/LightCullingHeatmap.pass
Passes/LightCullingParent.pass
@@ -152,6 +158,8 @@ set(FILES
Passes/LightCullingTilePrepareMSAA.pass
Passes/LookModificationComposite.pass
Passes/LookModificationTransform.pass
+ Passes/LowEndForward.pass
+ Passes/LowEndPipeline.pass
Passes/LuminanceHeatmap.pass
Passes/LuminanceHistogramGenerator.pass
Passes/MainPipeline.pass
@@ -179,13 +187,16 @@ set(FILES
Passes/ReflectionScreenSpace.pass
Passes/ReflectionScreenSpaceBlur.pass
Passes/ReflectionScreenSpaceBlurHorizontal.pass
+ Passes/ReflectionScreenSpaceBlurMobile.pass
Passes/ReflectionScreenSpaceBlurVertical.pass
Passes/ReflectionScreenSpaceComposite.pass
+ Passes/ReflectionScreenSpaceMobile.pass
Passes/ReflectionScreenSpaceTrace.pass
Passes/Reflections_nomsaa.pass
Passes/ShadowParent.pass
Passes/Skinning.pass
Passes/SkyBox.pass
+ Passes/SkyBox_TwoOutputs.pass
Passes/SMAA1xApplyLinearHDRColor.pass
Passes/SMAA1xApplyPerceptualColor.pass
Passes/SMAABlendingWeightCalculation.pass
@@ -205,6 +216,7 @@ set(FILES
ShaderLib/Atom/Features/IndirectRendering.azsli
ShaderLib/Atom/Features/MatrixUtility.azsli
ShaderLib/Atom/Features/ParallaxMapping.azsli
+ ShaderLib/Atom/Features/ShaderQualityOptions.azsli
ShaderLib/Atom/Features/SphericalHarmonicsUtility.azsli
ShaderLib/Atom/Features/SrgSemantics.azsli
ShaderLib/Atom/Features/ColorManagement/TransformColor.azsli
@@ -272,6 +284,7 @@ set(FILES
ShaderLib/Atom/Features/PostProcessing/GlyphData.azsli
ShaderLib/Atom/Features/PostProcessing/GlyphRender.azsli
ShaderLib/Atom/Features/PostProcessing/PostProcessUtil.azsli
+ ShaderLib/Atom/Features/RayTracing/RayTracingSceneSrg.azsli
ShaderLib/Atom/Features/ScreenSpace/ScreenSpaceUtil.azsli
ShaderLib/Atom/Features/Shadow/BicubicPcfFilters.azsli
ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli
@@ -471,4 +484,6 @@ set(FILES
Shaders/SkinnedMesh/LinearSkinningPassSRG.azsli
Shaders/SkyBox/SkyBox.azsl
Shaders/SkyBox/SkyBox.shader
+ Shaders/SkyBox/SkyBox_TwoOutputs.azsl
+ Shaders/SkyBox/SkyBox_TwoOutputs.shader
)
diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/ACES/AcesDisplayMapperFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/ACES/AcesDisplayMapperFeatureProcessor.h
index a89653c359..03fbb93923 100644
--- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/ACES/AcesDisplayMapperFeatureProcessor.h
+++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/ACES/AcesDisplayMapperFeatureProcessor.h
@@ -60,6 +60,12 @@ namespace AZ
: public DisplayMapperFeatureProcessorInterface
{
public:
+ enum OutputDeviceTransformFlags
+ {
+ AlterSurround = 0x1, // Apply gamma adjustment to compensate for dim surround
+ ApplyDesaturation = 0x2, // Apply desaturation to compensate for luminance difference
+ ApplyCATD60toD65 = 0x4, // Apply Color appearance transform (CAT) from ACES white point to assumed observer adapted white point
+ };
AZ_RTTI(AZ::Render::AcesDisplayMapperFeatureProcessor, "{995C2B93-8B08-4313-89B0-02394F90F1B8}", AZ::Render::DisplayMapperFeatureProcessorInterface);
@@ -92,12 +98,6 @@ namespace AZ
static void ApplyLdrOdtParameters(DisplayMapperParameters* pOutParameters);
static void ApplyHdrOdtParameters(DisplayMapperParameters* pOutParameters, const OutputDeviceTransformType& odtType);
- enum OutputDeviceTransformFlags {
- AlterSurround = 0x1, // Apply gamma adjustment to compensate for dim surround
- ApplyDesaturation = 0x2, // Apply desaturation to compensate for luminance difference
- ApplyCATD60toD65 = 0x4, // Apply Color appearance transform (CAT) from ACES white point to assumed observer adapted white point
- };
-
enum OutputDeviceTransformMode {
Srgb = 0,
PerceptualQuantizer,
diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/DisplayMapper/AcesOutputTransformPass.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/DisplayMapper/AcesOutputTransformPass.h
index 5fce71fc07..5a0ccdb32a 100644
--- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/DisplayMapper/AcesOutputTransformPass.h
+++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/DisplayMapper/AcesOutputTransformPass.h
@@ -46,6 +46,7 @@ namespace AZ
static RPI::Ptr Create(const RPI::PassDescriptor& descriptor);
void SetDisplayBufferFormat(RHI::Format format);
+ void SetAcesParameterOverrides(const AcesParameterOverrides& acesParameterOverrides);
private:
explicit AcesOutputTransformPass(const RPI::PassDescriptor& descriptor);
@@ -65,6 +66,8 @@ namespace AZ
AZ::Render::DisplayMapperParameters m_displayMapperParameters = {};
RHI::Format m_displayBufferFormat = RHI::Format::Unknown;
+
+ AcesParameterOverrides m_acesParameterOverrides;
};
} // namespace Render
} // namespace AZ
diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/DisplayMapper/DisplayMapperConfigurationDescriptor.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/DisplayMapper/DisplayMapperConfigurationDescriptor.h
index b645df4f6f..4dc090b831 100644
--- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/DisplayMapper/DisplayMapperConfigurationDescriptor.h
+++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/DisplayMapper/DisplayMapperConfigurationDescriptor.h
@@ -24,6 +24,48 @@ namespace AZ
namespace Render
{
+ /**
+ * The ACES display mapper parameter overrides.
+ * These parameters override default ACES parameters when m_overrideDefaults is true.
+ */
+ struct AcesParameterOverrides final
+ {
+ AZ_TYPE_INFO(AcesParameterOverrides, "{3EE8C0D4-3792-46C0-B91C-B89A81C36B91}");
+ static void Reflect(ReflectContext* context);
+
+ void LoadPreset();
+
+ // When enabled allows parameter overrides for ACES configuration
+ bool m_overrideDefaults = false;
+
+ // Apply gamma adjustment to compensate for dim surround
+ bool m_alterSurround = true;
+ // Apply desaturation to compensate for luminance difference
+ bool m_applyDesaturation = true;
+ // Apply Color appearance transform (CAT) from ACES white point to assumed observer adapted white point
+ bool m_applyCATD60toD65 = true;
+
+ // Reference white and black luminance values
+ float m_cinemaLimitsBlack = 0.02f;
+ float m_cinemaLimitsWhite = 48.0f;
+
+ // luminance linear extension below this
+ float m_minPoint = 0.0028798957f;
+ // luminance mid grey
+ float m_midPoint = 4.8f;
+ // luminance linear extension above this
+ float m_maxPoint = 1005.71912f;
+
+ // Gamma adjustment to be applied to compensate for the condition of the viewing environment.
+ // Note that ACES uses a value of 0.9811 for adjusting from dark to dim surrounding.
+ float m_surroundGamma = 0.9811f;
+ // Optional gamma value that is applied as basic gamma curve OETF
+ float m_gamma = 2.2f;
+
+ // Allows specifying default preset for different ODT modes
+ OutputDeviceTransformType m_preset = OutputDeviceTransformType_48Nits;
+ };
+
//! A descriptor used to configure the DisplayMapper
struct DisplayMapperConfigurationDescriptor final
{
@@ -37,6 +79,8 @@ namespace AZ
bool m_ldrGradingLutEnabled = false;
Data::Asset m_ldrColorGradingLut;
+
+ AcesParameterOverrides m_acesParameterOverrides;
};
//! Custom pass data for DisplayMapperPass.
diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h
index 7875e38fc0..0d61ef82d1 100644
--- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h
+++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h
@@ -148,6 +148,8 @@ namespace AZ
Data::Instance GetModel(const MeshHandle& meshHandle) const override;
Data::Asset GetModelAsset(const MeshHandle& meshHandle) const override;
+ Data::Instance GetObjectSrg(const MeshHandle& meshHandle) const override;
+ void QueueObjectSrgForCompile(const MeshHandle& meshHandle) const override;
void SetMaterialAssignmentMap(const MeshHandle& meshHandle, const Data::Instance& material) override;
void SetMaterialAssignmentMap(const MeshHandle& meshHandle, const MaterialAssignmentMap& materials) override;
const MaterialAssignmentMap& GetMaterialAssignmentMap(const MeshHandle& meshHandle) const override;
diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessorInterface.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessorInterface.h
index c2360068de..fb5bff5584 100644
--- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessorInterface.h
+++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessorInterface.h
@@ -61,6 +61,14 @@ namespace AZ
virtual Data::Instance GetModel(const MeshHandle& meshHandle) const = 0;
//! Gets the underlying RPI::ModelAsset for a meshHandle.
virtual Data::Asset GetModelAsset(const MeshHandle& meshHandle) const = 0;
+ //! Gets the ObjectSrg for a meshHandle.
+ //! Updating the ObjectSrg should be followed by a call to QueueObjectSrgForCompile,
+ //! instead of compiling the srg directly. This way, if the srg has already been queued for compile,
+ //! it will not be queued twice in the same frame. The ObjectSrg should not be updated during
+ //! Simulate, or it will create a race between updating the data and the call to Compile
+ virtual Data::Instance GetObjectSrg(const MeshHandle& meshHandle) const = 0;
+ //! Queues the object srg for compile.
+ virtual void QueueObjectSrgForCompile(const MeshHandle& meshHandle) const = 0;
//! Sets the MaterialAssignmentMap for a meshHandle, using just a single material for the DefaultMaterialAssignmentId.
//! Note if there is already a material assignment map, this will replace the entire map with just a single material.
virtual void SetMaterialAssignmentMap(const MeshHandle& meshHandle, const Data::Instance& material) = 0;
diff --git a/Gems/Atom/Feature/Common/Code/Mocks/MockMeshFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Mocks/MockMeshFeatureProcessor.h
index 39fd7b4380..418ee0cfb8 100644
--- a/Gems/Atom/Feature/Common/Code/Mocks/MockMeshFeatureProcessor.h
+++ b/Gems/Atom/Feature/Common/Code/Mocks/MockMeshFeatureProcessor.h
@@ -23,6 +23,8 @@ namespace UnitTest
MOCK_METHOD1(CloneMesh, MeshHandle(const MeshHandle&));
MOCK_CONST_METHOD1(GetModel, AZStd::intrusive_ptr(const MeshHandle&));
MOCK_CONST_METHOD1(GetModelAsset, AZ::Data::Asset(const MeshHandle&));
+ MOCK_CONST_METHOD1(GetObjectSrg, AZStd::intrusive_ptr(const MeshHandle&));
+ MOCK_CONST_METHOD1(QueueObjectSrgForCompile, void(const MeshHandle&));
MOCK_CONST_METHOD1(GetMaterialAssignmentMap, const AZ::Render::MaterialAssignmentMap&(const MeshHandle&));
MOCK_METHOD2(ConnectModelChangeEventHandler, void(const MeshHandle&, ModelChangedEvent::Handler&));
MOCK_METHOD3(SetTransform, void(const MeshHandle&, const AZ::Transform&, const AZ::Vector3&));
diff --git a/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/AcesOutputTransformPass.cpp b/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/AcesOutputTransformPass.cpp
index b2103739ba..6fe38ff032 100644
--- a/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/AcesOutputTransformPass.cpp
+++ b/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/AcesOutputTransformPass.cpp
@@ -102,6 +102,36 @@ namespace AZ
AcesDisplayMapperFeatureProcessor::GetAcesDisplayMapperParameters(&m_displayMapperParameters, OutputDeviceTransformType_48Nits);
}
}
+
+ if (m_acesParameterOverrides.m_overrideDefaults)
+ {
+ m_displayMapperParameters.m_OutputDisplayTransformFlags = 0;
+ if (m_acesParameterOverrides.m_alterSurround)
+ {
+ m_displayMapperParameters.m_OutputDisplayTransformFlags |= AcesDisplayMapperFeatureProcessor::AlterSurround;
+ }
+ if (m_acesParameterOverrides.m_applyDesaturation)
+ {
+ m_displayMapperParameters.m_OutputDisplayTransformFlags |= AcesDisplayMapperFeatureProcessor::ApplyDesaturation;
+ }
+ if (m_acesParameterOverrides.m_applyCATD60toD65)
+ {
+ m_displayMapperParameters.m_OutputDisplayTransformFlags |= AcesDisplayMapperFeatureProcessor::ApplyCATD60toD65;
+ }
+
+ m_displayMapperParameters.m_cinemaLimits[0] = m_acesParameterOverrides.m_cinemaLimitsBlack;
+ m_displayMapperParameters.m_cinemaLimits[1] = m_acesParameterOverrides.m_cinemaLimitsWhite;
+ m_displayMapperParameters.m_acesSplineParams.minPoint[0] = m_acesParameterOverrides.m_minPoint;
+ m_displayMapperParameters.m_acesSplineParams.midPoint[0] = m_acesParameterOverrides.m_midPoint;
+ m_displayMapperParameters.m_acesSplineParams.maxPoint[0] = m_acesParameterOverrides.m_maxPoint;
+ m_displayMapperParameters.m_surroundGamma = m_acesParameterOverrides.m_surroundGamma;
+ m_displayMapperParameters.m_gamma = m_acesParameterOverrides.m_gamma;
+ }
+ }
+
+ void AcesOutputTransformPass::SetAcesParameterOverrides(const AcesParameterOverrides& acesParameterOverrides)
+ {
+ m_acesParameterOverrides = acesParameterOverrides;
}
} // namespace Render
} // namespace AZ
diff --git a/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/DisplayMapperConfigurationDescriptor.cpp b/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/DisplayMapperConfigurationDescriptor.cpp
index d80a02d083..e91e125b40 100644
--- a/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/DisplayMapperConfigurationDescriptor.cpp
+++ b/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/DisplayMapperConfigurationDescriptor.cpp
@@ -9,14 +9,54 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*/
+
#include
#include
+#include
namespace AZ
{
namespace Render
{
+ void AcesParameterOverrides::Reflect(ReflectContext* context)
+ {
+ if (auto serializeContext = azrtti_cast(context))
+ {
+ serializeContext->Class()
+ ->Version(0)
+ ->Field("OverrideDefaults", &AcesParameterOverrides::m_overrideDefaults)
+ ->Field("AlterSurround", &AcesParameterOverrides::m_alterSurround)
+ ->Field("ApplyDesaturation", &AcesParameterOverrides::m_applyDesaturation)
+ ->Field("ApplyCATD60toD65", &AcesParameterOverrides::m_applyCATD60toD65)
+ ->Field("PresetODT", &AcesParameterOverrides::m_preset)
+ ->Field("CinemaLimitsBlack", &AcesParameterOverrides::m_cinemaLimitsBlack)
+ ->Field("CinemaLimitsWhite", &AcesParameterOverrides::m_cinemaLimitsWhite)
+ ->Field("MinPoint", &AcesParameterOverrides::m_minPoint)
+ ->Field("MidPoint", &AcesParameterOverrides::m_midPoint)
+ ->Field("MaxPoint", &AcesParameterOverrides::m_maxPoint)
+ ->Field("SurroundGamma", &AcesParameterOverrides::m_surroundGamma)
+ ->Field("Gamma", &AcesParameterOverrides::m_gamma);
+ }
+ }
+
+ void AcesParameterOverrides::LoadPreset()
+ {
+ DisplayMapperParameters displayMapperParameters;
+ AcesDisplayMapperFeatureProcessor::GetAcesDisplayMapperParameters(&displayMapperParameters, m_preset);
+
+ m_alterSurround = (displayMapperParameters.m_OutputDisplayTransformFlags & AcesDisplayMapperFeatureProcessor::AlterSurround) != 0;
+ m_applyDesaturation = (displayMapperParameters.m_OutputDisplayTransformFlags & AcesDisplayMapperFeatureProcessor::ApplyDesaturation) != 0;
+ m_applyCATD60toD65 = (displayMapperParameters.m_OutputDisplayTransformFlags & AcesDisplayMapperFeatureProcessor::ApplyCATD60toD65) != 0;
+ m_cinemaLimitsBlack = displayMapperParameters.m_cinemaLimits[0];
+ m_cinemaLimitsWhite = displayMapperParameters.m_cinemaLimits[1];
+ m_minPoint = displayMapperParameters.m_acesSplineParams.minPoint[0];
+ m_midPoint = displayMapperParameters.m_acesSplineParams.midPoint[0];
+ m_maxPoint = displayMapperParameters.m_acesSplineParams.maxPoint[0];
+ m_surroundGamma = displayMapperParameters.m_surroundGamma;
+ m_gamma = displayMapperParameters.m_gamma;
+ }
+
void DisplayMapperConfigurationDescriptor::Reflect(AZ::ReflectContext* context)
{
if (auto* serializeContext = azrtti_cast(context))
diff --git a/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/DisplayMapperPass.cpp b/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/DisplayMapperPass.cpp
index 1de92c42f6..8ae790e12c 100644
--- a/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/DisplayMapperPass.cpp
+++ b/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/DisplayMapperPass.cpp
@@ -106,6 +106,7 @@ namespace AZ
{
if (m_acesOutputTransformPass)
{
+ m_acesOutputTransformPass->SetAcesParameterOverrides(m_displayMapperConfigurationDescriptor.m_acesParameterOverrides);
m_acesOutputTransformPass->SetDisplayBufferFormat(m_displayBufferFormat);
}
if (m_bakeAcesOutputTransformLutPass)
@@ -509,7 +510,8 @@ namespace AZ
if (desc.m_operationType != m_displayMapperConfigurationDescriptor.m_operationType ||
desc.m_ldrGradingLutEnabled != m_displayMapperConfigurationDescriptor.m_ldrGradingLutEnabled ||
- desc.m_ldrColorGradingLut != m_displayMapperConfigurationDescriptor.m_ldrColorGradingLut)
+ desc.m_ldrColorGradingLut != m_displayMapperConfigurationDescriptor.m_ldrColorGradingLut ||
+ desc.m_acesParameterOverrides.m_overrideDefaults != m_displayMapperConfigurationDescriptor.m_acesParameterOverrides.m_overrideDefaults)
{
m_needToRebuildChildren = true;
}
diff --git a/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp
index 29f0636e9e..4059d65cbb 100644
--- a/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp
+++ b/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp
@@ -231,6 +231,19 @@ namespace AZ
return {};
}
+ Data::Instance MeshFeatureProcessor::GetObjectSrg(const MeshHandle& meshHandle) const
+ {
+ return meshHandle.IsValid() ? meshHandle->m_shaderResourceGroup : nullptr;
+ }
+
+ void MeshFeatureProcessor::QueueObjectSrgForCompile(const MeshHandle& meshHandle) const
+ {
+ if (meshHandle.IsValid())
+ {
+ meshHandle->m_objectSrgNeedsUpdate = true;
+ }
+ }
+
void MeshFeatureProcessor::SetMaterialAssignmentMap(const MeshHandle& meshHandle, const Data::Instance& material)
{
Render::MaterialAssignmentMap materials;
diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp
index 388f4112a0..cb2d6a69ef 100644
--- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp
+++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp
@@ -71,16 +71,6 @@ namespace AZ
}
- void SkinnedMeshFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet)
- {
- AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender);
- AZ_ATOM_PROFILE_FUNCTION("SkinnedMesh", "SkinnedMeshFeatureProcessor: Simulate");
- AZ_UNUSED(packet);
-
- SkinnedMeshFeatureProcessorNotificationBus::Broadcast(&SkinnedMeshFeatureProcessorNotificationBus::Events::OnUpdateSkinningMatrices);
-
- }
-
void SkinnedMeshFeatureProcessor::Render(const FeatureProcessor::RenderPacket& packet)
{
AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender);
@@ -268,6 +258,8 @@ namespace AZ
void SkinnedMeshFeatureProcessor::OnBeginPrepareRender()
{
m_renderProxiesChecker.soft_lock();
+
+ SkinnedMeshFeatureProcessorNotificationBus::Broadcast(&SkinnedMeshFeatureProcessorNotificationBus::Events::OnUpdateSkinningMatrices);
}
void SkinnedMeshFeatureProcessor::OnRenderEnd()
diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.h
index bb3dd242a1..75d41742b2 100644
--- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.h
+++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.h
@@ -49,7 +49,6 @@ namespace AZ
// FeatureProcessor overrides ...
void Activate() override;
void Deactivate() override;
- void Simulate(const FeatureProcessor::SimulatePacket& packet) override;
void Render(const FeatureProcessor::RenderPacket& packet) override;
void OnRenderEnd() override;
diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Pass.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Pass.h
index b0d6bd4117..1cba71ae7e 100644
--- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Pass.h
+++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Pass.h
@@ -381,6 +381,7 @@ namespace AZ
uint64_t m_createdByPassRequest : 1;
uint64_t m_initialized : 1;
uint64_t m_enabled : 1;
+ uint64_t m_parentEnabled : 1;
uint64_t m_alreadyCreated : 1;
uint64_t m_alreadyReset : 1;
uint64_t m_alreadyPrepared : 1;
diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/MorphTargetMetaAsset.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/MorphTargetMetaAsset.h
index 5b92047226..4aa3faa6c9 100644
--- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/MorphTargetMetaAsset.h
+++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/MorphTargetMetaAsset.h
@@ -15,6 +15,7 @@
#include
#include
#include
+#include
namespace AZ::RPI
{
@@ -56,6 +57,9 @@ namespace AZ::RPI
float m_minPositionDelta;
float m_maxPositionDelta;
+ //! Reference to the wrinkle mask, if it exists
+ AZ::Data::Asset m_wrinkleMask;
+
//! Boolean to indicate the presence or absence of color deltas
bool m_hasColorDeltas = false;
diff --git a/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.cpp b/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.cpp
index 7aace50760..3d0cbca8e6 100644
--- a/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.cpp
+++ b/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.cpp
@@ -18,6 +18,9 @@
#include
#include
+#include
+#include
+
namespace AZ::RPI
{
using namespace AZ::SceneAPI;
@@ -114,7 +117,7 @@ namespace AZ::RPI
meshNodeName, sourceMesh.m_name.GetCStr());
const DataTypes::MatrixType globalTransform = Utilities::BuildWorldTransform(sceneGraph, sceneNodeIndex);
- BuildMorphTargetMesh(vertexOffset, sourceMesh, productMesh, metaAssetCreator, blendShapeName, blendShapeData, globalTransform, coordSysConverter);
+ BuildMorphTargetMesh(vertexOffset, sourceMesh, productMesh, metaAssetCreator, blendShapeName, blendShapeData, globalTransform, coordSysConverter, scene.GetSourceFilename());
}
}
}
@@ -157,7 +160,8 @@ namespace AZ::RPI
const AZStd::string& blendShapeName,
const AZStd::shared_ptr& blendShapeData,
const DataTypes::MatrixType& globalTransform,
- const AZ::SceneAPI::CoordinateSystemConverter& coordSysConverter)
+ const AZ::SceneAPI::CoordinateSystemConverter& coordSysConverter,
+ const AZStd::string& sourceSceneFilename)
{
const float tolerance = CalcPositionDeltaTolerance(sourceMesh);
AZ::Aabb deltaPositionAabb = AZ::Aabb::CreateNull();
@@ -288,6 +292,8 @@ namespace AZ::RPI
metaData.m_maxPositionDelta = maxValue;
}
+ metaData.m_wrinkleMask = GetWrinkleMask(sourceSceneFilename, blendShapeName);
+
metaAssetCreator.AddMorphTarget(metaData);
AZ_Assert(uncompressedPositionDeltas.size() == compressedDeltas.size(), "Number of uncompressed (%d) and compressed position delta components (%d) do not match.",
@@ -312,4 +318,47 @@ namespace AZ::RPI
AZ_Assert((packedCompressedMorphTargetVertexData.size() - metaData.m_startIndex) == numMorphedVertices, "Vertex index range (%d) in morph target meta data does not match number of morphed vertices (%d).",
packedCompressedMorphTargetVertexData.size() - metaData.m_startIndex, numMorphedVertices);
}
+
+ Data::Asset MorphTargetExporter::GetWrinkleMask(const AZStd::string& sourceSceneFullFilePath, const AZStd::string& blendShapeName) const
+ {
+ AZ::Data::Asset imageAsset;
+
+ // See if there is a wrinkle map mask for this mesh
+ AZStd::string sceneRelativeFilePath;
+ bool relativePathFound = true;
+ AzToolsFramework::AssetSystemRequestBus::BroadcastResult(relativePathFound, &AzToolsFramework::AssetSystemRequestBus::Events::GetRelativeProductPathFromFullSourceOrProductPath, sourceSceneFullFilePath, sceneRelativeFilePath);
+
+ if (relativePathFound)
+ {
+ AZ::StringFunc::Path::StripFullName(sceneRelativeFilePath);
+
+ // Get the folder the masks are supposed to be in
+ AZStd::string folderName;
+ AZ::StringFunc::Path::GetFileName(sourceSceneFullFilePath.c_str(), folderName);
+ folderName += "_wrinklemasks";
+
+ // Note: for now, we're assuming the mask is always authored as a .tif
+ AZStd::string blendMaskFileName = blendShapeName + "_wrinklemask.tif.streamingimage";
+
+ AZStd::string maskFolderAndFile;
+ AZ::StringFunc::Path::Join(folderName.c_str(), blendMaskFileName.c_str(), maskFolderAndFile);
+
+ AZStd::string maskRelativePath;
+ AZ::StringFunc::Path::Join(sceneRelativeFilePath.c_str(), maskFolderAndFile.c_str(), maskRelativePath);
+ AZ::StringFunc::Path::Normalize(maskRelativePath);
+
+ // Now see if the file exists
+ AZ::Data::AssetId maskAssetId;
+ Data::AssetCatalogRequestBus::BroadcastResult(maskAssetId, &Data::AssetCatalogRequests::GetAssetIdByPath, maskRelativePath.c_str(), AZ::Data::s_invalidAssetType, false);
+
+ if (maskAssetId.IsValid())
+ {
+ // Flush asset manager events to ensure no asset references are held by closures queued on Ebuses.
+ AZ::Data::AssetManager::Instance().DispatchEvents();
+
+ imageAsset.Create(maskAssetId, AZ::Data::AssetLoadBehavior::PreLoad, false);
+ }
+ }
+ return imageAsset;
+ }
} // namespace AZ::RPI
diff --git a/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.h b/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.h
index d968a803d6..4845d7d1da 100644
--- a/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.h
+++ b/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.h
@@ -64,7 +64,11 @@ namespace AZ
const AZStd::string& blendShapeName,
const AZStd::shared_ptr& blendShapeData,
const AZ::SceneAPI::DataTypes::MatrixType& globalTransform,
- const AZ::SceneAPI::CoordinateSystemConverter& coordSysConverter);
+ const AZ::SceneAPI::CoordinateSystemConverter& coordSysConverter,
+ const AZStd::string& sourceSceneFilename);
+
+ // Find a wrinkle mask for this morph target, if it exists
+ Data::Asset GetWrinkleMask(const AZStd::string& sourceSceneFullFilePath, const AZStd::string& blendShapeName) const;
};
} // namespace RPI
} // namespace AZ
diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp
index 2ab6ee92e9..109af70166 100644
--- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp
+++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp
@@ -422,7 +422,7 @@ namespace AZ
const MaterialPropertyDescriptor* propertyDescriptor = materialTypeAssetCreator.GetMaterialPropertiesLayout()->GetPropertyDescriptor(propertyIndex);
AZ::Name enumName = AZ::Name(property.m_value.GetValue());
- uint32_t enumValue = propertyDescriptor->GetEnumValue(enumName);
+ uint32_t enumValue = propertyDescriptor ? propertyDescriptor->GetEnumValue(enumName) : MaterialPropertyDescriptor::InvalidEnumValue;
if (enumValue == MaterialPropertyDescriptor::InvalidEnumValue)
{
materialTypeAssetCreator.ReportError("Enum value '%s' couldn't be found in the 'enumValues' list", enumName.GetCStr());
diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp
index 9401d1a9e0..6ed8ac018c 100644
--- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp
+++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp
@@ -93,11 +93,12 @@ namespace AZ
void Pass::SetEnabled(bool enabled)
{
m_flags.m_enabled = enabled;
+ OnHierarchyChange();
}
bool Pass::IsEnabled() const
{
- return m_flags.m_enabled;
+ return m_flags.m_enabled && (m_flags.m_parentEnabled || m_parent == nullptr);
}
// --- Error Logging ---
@@ -140,6 +141,7 @@ namespace AZ
}
// Set new tree depth and path
+ m_flags.m_parentEnabled = m_parent->m_flags.m_enabled && (m_parent->m_flags.m_parentEnabled || m_parent->m_parent == nullptr);
m_treeDepth = m_parent->m_treeDepth + 1;
m_path = ConcatPassName(m_parent->m_path, m_name);
m_flags.m_partOfHierarchy = m_parent->m_flags.m_partOfHierarchy;
diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp
index 1fad385fa6..3f5c16678c 100644
--- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp
+++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp
@@ -163,6 +163,11 @@ namespace AZ
binding.m_shaderInputIndex = idx.IsValid() ? static_cast(idx.GetIndex()) : PassAttachmentBinding::ShaderInputNoBind;
}
}
+ else
+ {
+ AZ_Error("Pass System", false, "[Pass %s] Could not bind shader buffer index '%s' because it has no attachment.", GetName().GetCStr(), shaderName.GetCStr());
+ binding.m_shaderInputIndex = PassAttachmentBinding::ShaderInputNoBind;
+ }
}
}
diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/MorphTargetMetaAsset.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/MorphTargetMetaAsset.cpp
index 313e0bea31..3c0f832807 100644
--- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/MorphTargetMetaAsset.cpp
+++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/MorphTargetMetaAsset.cpp
@@ -28,6 +28,7 @@ namespace AZ::RPI
->Field("numVertices", &MorphTargetMetaAsset::MorphTarget::m_numVertices)
->Field("minPositionDelta", &MorphTargetMetaAsset::MorphTarget::m_minPositionDelta)
->Field("maxPositionDelta", &MorphTargetMetaAsset::MorphTarget::m_maxPositionDelta)
+ ->Field("wrinkleMask", &MorphTargetMetaAsset::MorphTarget::m_wrinkleMask)
->Field("hasColorDeltas", &MorphTargetMetaAsset::MorphTarget::m_hasColorDeltas)
;
}
diff --git a/Gems/Atom/TestData/TestData/Materials/SkinTestCases/001_lucy_regression_test.material b/Gems/Atom/TestData/TestData/Materials/SkinTestCases/001_lucy_regression_test.material
index f43f6d0808..c359fea3b5 100644
--- a/Gems/Atom/TestData/TestData/Materials/SkinTestCases/001_lucy_regression_test.material
+++ b/Gems/Atom/TestData/TestData/Materials/SkinTestCases/001_lucy_regression_test.material
@@ -11,7 +11,7 @@
0.29372090101242068,
1.0
],
- "textureMap": "Objects/Lucy/Lucy_brass_baseColor.tif",
+ "textureMap": "Objects/Lucy/Lucy_bronze_BaseColor.png",
"useTexture": false
},
"detailLayerGroup": {
@@ -30,7 +30,7 @@
},
"normal": {
"flipY": true,
- "textureMap": "Objects/Lucy/Lucy_normal.tif"
+ "textureMap": "Objects/Lucy/Lucy_normal.png"
},
"subsurfaceScattering": {
"enableSubsurfaceScattering": true,
diff --git a/Gems/Atom/TestData/TestData/Materials/SkinTestCases/002_wrinkle_regression_test.material b/Gems/Atom/TestData/TestData/Materials/SkinTestCases/002_wrinkle_regression_test.material
index c611b992b6..ce42f32b67 100644
--- a/Gems/Atom/TestData/TestData/Materials/SkinTestCases/002_wrinkle_regression_test.material
+++ b/Gems/Atom/TestData/TestData/Materials/SkinTestCases/002_wrinkle_regression_test.material
@@ -29,7 +29,7 @@
},
"normal": {
"flipY": true,
- "textureMap": "Objects/Lucy/Lucy_normal.tif"
+ "textureMap": "Objects/Lucy/Lucy_normal.png"
},
"subsurfaceScattering": {
"enableSubsurfaceScattering": true,
diff --git a/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/101_DetailMaps_LucyBaseNoDetailMaps.material b/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/101_DetailMaps_LucyBaseNoDetailMaps.material
index 2c711a3bf3..7b1f0ba6a9 100644
--- a/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/101_DetailMaps_LucyBaseNoDetailMaps.material
+++ b/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/101_DetailMaps_LucyBaseNoDetailMaps.material
@@ -5,20 +5,20 @@
"propertyLayoutVersion": 3,
"properties": {
"baseColor": {
- "textureMap": "Objects/Lucy/Lucy_brass_baseColor.tif",
+ "textureMap": "Objects/Lucy/Lucy_bronze_BaseColor.png",
"textureMapUv": "Unwrapped"
},
"metallic": {
- "textureMap": "Objects/Lucy/Lucy_brass_metalness.tif",
+ "textureMap": "Objects/Lucy/Lucy_bronze_metallic.png",
"textureMapUv": "Unwrapped"
},
"normal": {
"flipY": true,
- "textureMap": "Objects/Lucy/Lucy_normal.tif",
+ "textureMap": "Objects/Lucy/Lucy_normal.png",
"textureMapUv": "Unwrapped"
},
"roughness": {
- "textureMap": "Objects/Lucy/Lucy_brass_roughness.tif",
+ "textureMap": "Objects/Lucy/Lucy_bronze_roughness.png",
"textureMapUv": "Unwrapped"
}
}
diff --git a/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/102_DetailMaps_All.material b/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/102_DetailMaps_All.material
index 7a94386a18..55a01866b5 100644
--- a/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/102_DetailMaps_All.material
+++ b/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/102_DetailMaps_All.material
@@ -5,7 +5,7 @@
"propertyLayoutVersion": 3,
"properties": {
"baseColor": {
- "textureMap": "Objects/Lucy/Lucy_brass_baseColor.tif",
+ "textureMap": "Objects/Lucy/Lucy_bronze_baseColor.png",
"textureMapUv": "Unwrapped"
},
"detailLayerGroup": {
@@ -22,16 +22,16 @@
"scale": 10.0
},
"metallic": {
- "textureMap": "Objects/Lucy/Lucy_brass_metalness.tif",
+ "textureMap": "Objects/Lucy/Lucy_bronze_metallic.png",
"textureMapUv": "Unwrapped"
},
"normal": {
"flipY": true,
- "textureMap": "Objects/Lucy/Lucy_normal.tif",
+ "textureMap": "Objects/Lucy/Lucy_normal.png",
"textureMapUv": "Unwrapped"
},
"roughness": {
- "textureMap": "Objects/Lucy/Lucy_brass_roughness.tif",
+ "textureMap": "Objects/Lucy/Lucy_bronze_roughness.png",
"textureMapUv": "Unwrapped"
}
}
diff --git a/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/105_DetailMaps_BlendMaskUsingDetailUVs.material b/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/105_DetailMaps_BlendMaskUsingDetailUVs.material
index ddeace43da..6193cf4eed 100644
--- a/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/105_DetailMaps_BlendMaskUsingDetailUVs.material
+++ b/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/105_DetailMaps_BlendMaskUsingDetailUVs.material
@@ -5,7 +5,7 @@
"propertyLayoutVersion": 3,
"properties": {
"baseColor": {
- "textureMap": "Objects/Lucy/Lucy_brass_baseColor.tif",
+ "textureMap": "Objects/Lucy/Lucy_bronze_baseColor.png",
"textureMapUv": "Unwrapped"
},
"detailLayerGroup": {
@@ -21,16 +21,16 @@
"scale": 10.0
},
"metallic": {
- "textureMap": "Objects/Lucy/Lucy_brass_metalness.tif",
+ "textureMap": "Objects/Lucy/Lucy_bronze_metallic.png",
"textureMapUv": "Unwrapped"
},
"normal": {
"flipY": true,
- "textureMap": "Objects/Lucy/Lucy_normal.tif",
+ "textureMap": "Objects/Lucy/Lucy_normal.png",
"textureMapUv": "Unwrapped"
},
"roughness": {
- "textureMap": "Objects/Lucy/Lucy_brass_roughness.tif",
+ "textureMap": "Objects/Lucy/Lucy_bronze_roughness.png",
"textureMapUv": "Unwrapped"
}
}
diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/PostProcess/DisplayMapper/DisplayMapperComponentConfig.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/PostProcess/DisplayMapper/DisplayMapperComponentConfig.h
index 81645b25a9..3c5bcbea00 100644
--- a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/PostProcess/DisplayMapper/DisplayMapperComponentConfig.h
+++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/PostProcess/DisplayMapper/DisplayMapperComponentConfig.h
@@ -20,7 +20,6 @@ namespace AZ
{
namespace Render
{
-
class DisplayMapperComponentConfig final
: public ComponentConfig
{
@@ -33,6 +32,7 @@ namespace AZ
DisplayMapperOperationType m_displayMapperOperation = DisplayMapperOperationType::Aces;
bool m_ldrColorGradingLutEnabled = false;
Data::Asset m_ldrColorGradingLut = {};
+ AcesParameterOverrides m_acesParameterOverrides;
};
}
}
diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/DisplayMapper/DisplayMapperComponentConfig.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/DisplayMapper/DisplayMapperComponentConfig.cpp
index e6afd4f21f..317fa96450 100644
--- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/DisplayMapper/DisplayMapperComponentConfig.cpp
+++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/DisplayMapper/DisplayMapperComponentConfig.cpp
@@ -20,16 +20,18 @@ namespace AZ
{
void DisplayMapperComponentConfig::Reflect(ReflectContext* context)
{
+ AcesParameterOverrides::Reflect(context);
+
if (auto serializeContext = azrtti_cast(context))
{
serializeContext->Class()
- ->Version(0)
+ ->Version(1)
->Field("DisplayMapperOperationType", &DisplayMapperComponentConfig::m_displayMapperOperation)
->Field("LdrColorGradingLutEnabled", &DisplayMapperComponentConfig::m_ldrColorGradingLutEnabled)
->Field("LdrColorGradingLut", &DisplayMapperComponentConfig::m_ldrColorGradingLut)
+ ->Field("AcesParameterOverrides", &DisplayMapperComponentConfig::m_acesParameterOverrides)
;
}
}
-
} // namespace Render
} // namespace AZ
diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/DisplayMapper/DisplayMapperComponentController.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/DisplayMapper/DisplayMapperComponentController.cpp
index d90e170322..0e283199e7 100644
--- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/DisplayMapper/DisplayMapperComponentController.cpp
+++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/DisplayMapper/DisplayMapperComponentController.cpp
@@ -85,6 +85,7 @@ namespace AZ
desc.m_operationType = m_configuration.m_displayMapperOperation;
desc.m_ldrGradingLutEnabled = m_configuration.m_ldrColorGradingLutEnabled;
desc.m_ldrColorGradingLut = m_configuration.m_ldrColorGradingLut;
+ desc.m_acesParameterOverrides = m_configuration.m_acesParameterOverrides;
fp->RegisterDisplayMapperConfiguration(desc);
}
diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/DisplayMapper/EditorDisplayMapperComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/DisplayMapper/EditorDisplayMapperComponent.cpp
index 80aad79218..64cd450940 100644
--- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/DisplayMapper/EditorDisplayMapperComponent.cpp
+++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/DisplayMapper/EditorDisplayMapperComponent.cpp
@@ -10,6 +10,8 @@
*
*/
+#include "Atom/Feature/ACES/AcesDisplayMapperFeatureProcessor.h"
+
#include
#include
@@ -47,6 +49,76 @@ namespace AZ
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
;
+ editContext->Class(
+ "AcesParameterOverrides", "")
+ ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+ ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+
+ ->DataElement(
+ AZ::Edit::UIHandlers::CheckBox, &AcesParameterOverrides::m_overrideDefaults, "Override Defaults",
+ "When enabled allows parameter overrides for ACES configuration")
+ ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
+
+ ->DataElement(
+ AZ::Edit::UIHandlers::CheckBox, &AcesParameterOverrides::m_alterSurround, "Alter Surround",
+ "Apply gamma adjustment to compensate for dim surround")
+ ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
+ ->DataElement(
+ AZ::Edit::UIHandlers::CheckBox, &AcesParameterOverrides::m_applyDesaturation, "Alter Desaturation",
+ "Apply desaturation to compensate for luminance difference")
+ ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
+ ->DataElement(
+ AZ::Edit::UIHandlers::CheckBox, &AcesParameterOverrides::m_applyCATD60toD65, "Alter CAT D60 to D65",
+ "Apply Color appearance transform (CAT) from ACES white point to assumed observer adapted white point")
+ ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
+
+ ->DataElement(
+ Edit::UIHandlers::Default, &AcesParameterOverrides::m_cinemaLimitsBlack,
+ "Cinema Limit (black)",
+ "Reference black luminance value")
+ ->DataElement(
+ Edit::UIHandlers::Default, &AcesParameterOverrides::m_cinemaLimitsWhite,
+ "Cinema Limit (white)",
+ "Reference white luminance value")
+ ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
+
+ ->DataElement(
+ Edit::UIHandlers::Vector2, &AcesParameterOverrides::m_minPoint, "Min Point (luminance)",
+ "Linear extension below this")
+ ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
+ ->DataElement(
+ Edit::UIHandlers::Vector2, &AcesParameterOverrides::m_midPoint, "Mid Point (luminance)",
+ "Middle gray")
+ ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
+ ->DataElement(
+ Edit::UIHandlers::Vector2, &AcesParameterOverrides::m_maxPoint, "Max Point (luminance)",
+ "Linear extension above this")
+ ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
+
+ ->DataElement(
+ AZ::Edit::UIHandlers::Default, &AcesParameterOverrides::m_surroundGamma, "Surround Gamma",
+ "Gamma adjustment to be applied to compensate for the condition of the viewing environment")
+ ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
+ ->DataElement(
+ AZ::Edit::UIHandlers::Default, &AcesParameterOverrides::m_gamma, "Gamma",
+ "Optional gamma value that is applied as basic gamma curve OETF")
+ ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
+
+ // Load preset group
+ ->ClassElement(AZ::Edit::ClassElements::Group, "Load Preset")
+ ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+ ->DataElement(
+ Edit::UIHandlers::ComboBox, &AcesParameterOverrides::m_preset, "Preset Selection",
+ "Allows specifying default preset for different ODT modes")
+ ->EnumAttribute(OutputDeviceTransformType::OutputDeviceTransformType_48Nits, "48 Nits")
+ ->EnumAttribute(OutputDeviceTransformType::OutputDeviceTransformType_1000Nits, "1000 Nits")
+ ->EnumAttribute(OutputDeviceTransformType::OutputDeviceTransformType_2000Nits, "2000 Nits")
+ ->EnumAttribute(OutputDeviceTransformType::OutputDeviceTransformType_4000Nits, "4000 Nits")
+ ->UIElement(AZ::Edit::UIHandlers::Button, "Load", "Load default preset")
+ ->Attribute(AZ::Edit::Attributes::ChangeNotify, &AcesParameterOverrides::LoadPreset)
+ ->Attribute(AZ::Edit::Attributes::ButtonText, "Load")
+ ;
+
editContext->Class("ToneMapperComponentConfig", "")
->ClassElement(Edit::ClassElements::EditorData, "")
->DataElement(Edit::UIHandlers::ComboBox,
@@ -64,7 +136,10 @@ namespace AZ
&DisplayMapperComponentConfig::m_ldrColorGradingLutEnabled,
"Enable LDR color grading LUT",
"Enable LDR color grading LUT.")
- ->DataElement(AZ::Edit::UIHandlers::Default, &DisplayMapperComponentConfig::m_ldrColorGradingLut, "LDR color Grading LUT", "LDR color grading LUT");
+ ->DataElement(AZ::Edit::UIHandlers::Default, &DisplayMapperComponentConfig::m_ldrColorGradingLut, "LDR color Grading LUT", "LDR color grading LUT")
+ ->DataElement(AZ::Edit::UIHandlers::Default, &DisplayMapperComponentConfig::m_acesParameterOverrides, "ACES Parameters", "Parameter overrides for ACES.")
+ ;
+
}
}
diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp
index d0116452b7..9079f639ba 100644
--- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp
+++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp
@@ -28,6 +28,7 @@
#include
#include
+#include
#include
#include
@@ -39,6 +40,8 @@ namespace AZ
{
namespace Render
{
+ static constexpr uint32_t s_maxActiveWrinkleMasks = 16;
+
AZ_CLASS_ALLOCATOR_IMPL(AtomActorInstance, EMotionFX::Integration::EMotionFXAllocator, 0)
AtomActorInstance::AtomActorInstance(AZ::EntityId entityId,
@@ -413,6 +416,10 @@ namespace AZ
EMotionFX::MorphSetup* morphSetup = m_actorInstance->GetActor()->GetMorphSetup(lodIndex);
if (morphSetup)
{
+ // Track all the masks/weights that are currently active
+ m_wrinkleMasks.clear();
+ m_wrinkleMaskWeights.clear();
+
uint32_t morphTargetCount = morphSetup->GetNumMorphTargets();
m_morphTargetWeights.clear();
for (uint32_t morphTargetIndex = 0; morphTargetIndex < morphTargetCount; ++morphTargetIndex)
@@ -437,11 +444,28 @@ namespace AZ
const EMotionFX::MorphTargetStandard::DeformData* deformData = morphTargetStandard->GetDeformData(deformDataIndex);
if (deformData->mNumVerts > 0)
{
- m_morphTargetWeights.push_back(morphTargetSetupInstance->GetWeight());
+ float weight = morphTargetSetupInstance->GetWeight();
+ m_morphTargetWeights.push_back(weight);
+
+ // If the morph target is active and it has a wrinkle mask
+ auto wrinkleMaskIter = m_morphTargetWrinkleMaskMapsByLod[lodIndex].find(morphTargetStandard);
+ if (weight > 0 && wrinkleMaskIter != m_morphTargetWrinkleMaskMapsByLod[lodIndex].end())
+ {
+ // Add the wrinkle mask and weight, to be set on the material
+ m_wrinkleMasks.push_back(wrinkleMaskIter->second);
+ m_wrinkleMaskWeights.push_back(weight);
+ }
}
}
}
m_skinnedMeshRenderProxy->SetMorphTargetWeights(lodIndex, m_morphTargetWeights);
+
+ // Until EMotionFX and Atom lods are synchronized [ATOM-13564] we don't know which EMotionFX lod to pull the weights from
+ // Until that is fixed, just use lod 0 [ATOM-15251]
+ if (lodIndex == 0)
+ {
+ UpdateWrinkleMasks();
+ }
}
}
}
@@ -453,6 +477,8 @@ namespace AZ
MaterialComponentRequestBus::EventResult(materials, m_entityId, &MaterialComponentRequests::GetMaterialOverrides);
CreateRenderProxy(materials);
+ InitWrinkleMasks();
+
TransformNotificationBus::Handler::BusConnect(m_entityId);
MaterialComponentNotificationBus::Handler::BusConnect(m_entityId);
MeshComponentRequestBus::Handler::BusConnect(m_entityId);
@@ -573,5 +599,77 @@ namespace AZ
{
CreateSkinnedMeshInstance();
}
+
+ void AtomActorInstance::InitWrinkleMasks()
+ {
+ EMotionFX::Actor* actor = m_actorAsset->GetActor();
+ m_morphTargetWrinkleMaskMapsByLod.resize(m_skinnedMeshInputBuffers->GetLodCount());
+ m_wrinkleMasks.reserve(s_maxActiveWrinkleMasks);
+ m_wrinkleMaskWeights.reserve(s_maxActiveWrinkleMasks);
+
+ for (size_t lodIndex = 0; lodIndex < m_skinnedMeshInputBuffers->GetLodCount(); ++lodIndex)
+ {
+ EMotionFX::MorphSetup* morphSetup = actor->GetMorphSetup(lodIndex);
+ if (morphSetup)
+ {
+ const AZStd::vector& metaDatas = actor->GetMorphTargetMetaAsset()->GetMorphTargets();
+ // Loop over all the EMotionFX morph targets
+ uint32_t numMorphTargets = morphSetup->GetNumMorphTargets();
+ for (uint32_t morphTargetIndex = 0; morphTargetIndex < numMorphTargets; ++morphTargetIndex)
+ {
+ EMotionFX::MorphTargetStandard* morphTarget = static_cast(morphSetup->GetMorphTarget(morphTargetIndex));
+ for (const RPI::MorphTargetMetaAsset::MorphTarget& metaData : metaDatas)
+ {
+ // Find the metaData associated with this morph target
+ if (metaData.m_morphTargetName == morphTarget->GetNameString() && metaData.m_wrinkleMask && metaData.m_numVertices > 0)
+ {
+ // If the metaData has a wrinkle mask, add it to the map
+ Data::Instance streamingImage = RPI::StreamingImage::FindOrCreate(metaData.m_wrinkleMask);
+ if (streamingImage)
+ {
+ m_morphTargetWrinkleMaskMapsByLod[lodIndex][morphTarget] = streamingImage;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ void AtomActorInstance::UpdateWrinkleMasks()
+ {
+ if (m_meshHandle)
+ {
+ Data::Instance wrinkleMaskObjectSrg = m_meshFeatureProcessor->GetObjectSrg(*m_meshHandle);
+ if (wrinkleMaskObjectSrg)
+ {
+ RHI::ShaderInputImageIndex wrinkleMasksIndex = wrinkleMaskObjectSrg->FindShaderInputImageIndex(Name{ "m_wrinkle_masks" });
+ RHI::ShaderInputConstantIndex wrinkleMaskWeightsIndex = wrinkleMaskObjectSrg->FindShaderInputConstantIndex(Name{ "m_wrinkle_mask_weights" });
+ RHI::ShaderInputConstantIndex wrinkleMaskCountIndex = wrinkleMaskObjectSrg->FindShaderInputConstantIndex(Name{ "m_wrinkle_mask_count" });
+ if (wrinkleMasksIndex.IsValid() || wrinkleMaskWeightsIndex.IsValid() || wrinkleMaskCountIndex.IsValid())
+ {
+ AZ_Error("AtomActorInstance", wrinkleMasksIndex.IsValid(), "m_wrinkle_masks not found on the ObjectSrg, but m_wrinkle_mask_weights and/or m_wrinkle_mask_count are being used.");
+ AZ_Error("AtomActorInstance", wrinkleMaskWeightsIndex.IsValid(), "m_wrinkle_mask_weights not found on the ObjectSrg, but m_wrinkle_masks and/or m_wrinkle_mask_count are being used.");
+ AZ_Error("AtomActorInstance", wrinkleMaskCountIndex.IsValid(), "m_wrinkle_mask_count not found on the ObjectSrg, but m_wrinkle_mask_weights and/or m_wrinkle_masks are being used.");
+
+ if (m_wrinkleMasks.size())
+ {
+ wrinkleMaskObjectSrg->SetImageArray(wrinkleMasksIndex, AZStd::array_view>(m_wrinkleMasks.data(), m_wrinkleMasks.size()));
+
+ // Set the weights for any active masks
+ for (size_t i = 0; i < m_wrinkleMaskWeights.size(); ++i)
+ {
+ wrinkleMaskObjectSrg->SetConstant(wrinkleMaskWeightsIndex, m_wrinkleMaskWeights[i], i);
+ }
+ AZ_Error("AtomActorInstance", m_wrinkleMaskWeights.size() <= s_maxActiveWrinkleMasks, "The skinning shader supports no more than %d active morph targets with wrinkle masks.", s_maxActiveWrinkleMasks);
+ }
+
+ wrinkleMaskObjectSrg->SetConstant(wrinkleMaskCountIndex, aznumeric_cast(m_wrinkleMasks.size()));
+ m_meshFeatureProcessor->QueueObjectSrgForCompile(*m_meshHandle);
+ }
+ }
+ }
+ }
+
} //namespace Render
} // namespace AZ
diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.h b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.h
index 1002fcbde1..e05280e896 100644
--- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.h
+++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.h
@@ -17,6 +17,7 @@
#include
#include
+#include
#include
@@ -29,6 +30,8 @@
#include
#include
#include
+#include
+
#include
#include
@@ -41,6 +44,7 @@ namespace AZ::RPI
{
class Model;
class Buffer;
+ class StreamingImage;
}
namespace AZ
@@ -168,6 +172,11 @@ namespace AZ
// SkinnedMeshOutputStreamNotificationBus
void OnSkinnedMeshOutputStreamMemoryAvailable() override;
+ // Check to see if the skin material is being used,
+ // and if there are blend shapes with wrinkle masks that should be applied to it
+ void InitWrinkleMasks();
+ void UpdateWrinkleMasks();
+
AZStd::intrusive_ptr m_skinnedMeshInputBuffers = nullptr;
AZStd::intrusive_ptr m_skinnedMeshInstance;
AZ::Data::Instance m_boneTransforms = nullptr;
@@ -179,6 +188,12 @@ namespace AZ
AZ::TransformInterface* m_transformInterface = nullptr;
AZStd::set m_waitForMaterialLoadIds;
AZStd::vector m_morphTargetWeights;
+
+ typedef AZStd::unordered_map> MorphTargetWrinkleMaskMap;
+ AZStd::vector m_morphTargetWrinkleMaskMapsByLod;
+
+ AZStd::vector> m_wrinkleMasks;
+ AZStd::vector m_wrinkleMaskWeights;
};
} // namespace Render
diff --git a/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.cpp b/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.cpp
index 473e9534cb..6fa69c7a24 100644
--- a/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.cpp
+++ b/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.cpp
@@ -73,7 +73,10 @@ namespace PhysX
{
}
- CharacterControllerComponent::~CharacterControllerComponent() = default;
+ CharacterControllerComponent::~CharacterControllerComponent()
+ {
+ DisableController();
+ }
// AZ::Component
void CharacterControllerComponent::Init()
@@ -92,7 +95,7 @@ namespace PhysX
void CharacterControllerComponent::Deactivate()
{
- DestroyController();
+ DisableController();
Physics::CollisionFilteringRequestBus::Handler::BusDisconnect();
AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusDisconnect();
@@ -198,7 +201,7 @@ namespace PhysX
void CharacterControllerComponent::DisablePhysics()
{
- DestroyController();
+ DisableController();
}
bool CharacterControllerComponent::IsPhysicsEnabled() const
@@ -421,17 +424,32 @@ namespace PhysX
AZ::TransformBus::EventResult(entityTranslation, GetEntityId(), &AZ::TransformBus::Events::GetWorldTranslation);
m_characterConfig->m_position = entityTranslation;
- if (auto* sceneInterface = AZ::Interface::Get())
+ auto* sceneInterface = AZ::Interface::Get();
+ if (sceneInterface != nullptr)
{
- AzPhysics::SimulatedBodyHandle bodyHandle = sceneInterface->AddSimulatedBody(defaultSceneHandle, m_characterConfig.get());
- m_controller = azdynamic_cast(sceneInterface->GetSimulatedBodyFromHandle(defaultSceneHandle, bodyHandle));
+ m_controllerBodyHandle = sceneInterface->AddSimulatedBody(defaultSceneHandle, m_characterConfig.get());
+ m_controller = azdynamic_cast(
+ sceneInterface->GetSimulatedBodyFromHandle(defaultSceneHandle, m_controllerBodyHandle));
}
if (m_controller == nullptr)
{
AZ_Error("PhysX Character Controller Component", false, "Failed to create character controller.");
return;
}
-
+
+ if (sceneInterface != nullptr)
+ {
+ // if the scene removes this controller body, we should also clean up our resources.
+ m_onSimulatedBodyRemovedHandler = AzPhysics::SceneEvents::OnSimulationBodyRemoved::Handler(
+ [this]([[maybe_unused]] AzPhysics::SceneHandle sceneHandle, AzPhysics::SimulatedBodyHandle bodyHandle) {
+ if (bodyHandle == m_controllerBodyHandle)
+ {
+ DestroyController();
+ }
+ });
+ sceneInterface->RegisterSimulationBodyRemovedHandler(defaultSceneHandle, m_onSimulatedBodyRemovedHandler);
+ }
+
CharacterControllerRequestBus::Handler::BusConnect(GetEntityId());
m_preSimulateHandler = AzPhysics::SystemEvents::OnPresimulateEvent::Handler(
@@ -447,7 +465,7 @@ namespace PhysX
}
}
- void CharacterControllerComponent::DestroyController()
+ void CharacterControllerComponent::DisableController()
{
if (!IsPhysicsEnabled())
{
@@ -460,10 +478,15 @@ namespace PhysX
{
sceneInterface->RemoveSimulatedBody(m_controller->m_sceneOwner, m_controller->m_bodyHandle);
}
+
+ DestroyController();
+ }
+
+ void CharacterControllerComponent::DestroyController()
+ {
m_controller = nullptr;
-
m_preSimulateHandler.Disconnect();
-
+ m_onSimulatedBodyRemovedHandler.Disconnect();
CharacterControllerRequestBus::Handler::BusDisconnect();
}
} // namespace PhysX
diff --git a/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.h b/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.h
index a7a1a92ad2..7c25312b72 100644
--- a/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.h
+++ b/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.h
@@ -131,7 +131,12 @@ namespace PhysX
void ToggleCollisionLayer(const AZStd::string& layerName, AZ::Crc32 colliderTag, bool enabled) override;
private:
+ // Creates the physics character controller in the current default physics scene.
+ // This will do nothing if the controller is already created.
void CreateController();
+ // Removes the physics character controller from the scene and will call DestroyController for clean up.
+ void DisableController();
+ // Cleans up all references and events used with the physics character controller.
void DestroyController();
void OnPreSimulate(float deltaTime);
@@ -139,6 +144,8 @@ namespace PhysX
AZStd::unique_ptr m_characterConfig;
AZStd::shared_ptr m_shapeConfig;
PhysX::CharacterController* m_controller = nullptr;
+ AzPhysics::SimulatedBodyHandle m_controllerBodyHandle = AzPhysics::InvalidSimulatedBodyHandle;
AzPhysics::SystemEvents::OnPresimulateEvent::Handler m_preSimulateHandler;
+ AzPhysics::SceneEvents::OnSimulationBodyRemoved::Handler m_onSimulatedBodyRemovedHandler;
};
} // namespace PhysX
diff --git a/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp b/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp
index 79aa767959..689ea47be7 100644
--- a/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp
+++ b/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp
@@ -489,6 +489,7 @@ namespace PhysX
// Disable simulation on body (not signaling OnSimulationBodySimulationDisabled event)
DisableSimulationOfBodyInternal(*simulatedBody.second);
}
+ m_simulatedBodyRemovedEvent.Signal(m_sceneHandle, simulatedBody.second->m_bodyHandle);
delete simulatedBody.second;
}
}
diff --git a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp
index 592a8e2a75..45908eb2d5 100644
--- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp
+++ b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp
@@ -135,7 +135,7 @@ namespace AZ::SceneGenerationComponents
{
for (size_t controlPointIndex = 0; controlPointIndex < skinData.get().GetVertexCount(); ++controlPointIndex)
{
- const int usedPointIndex = meshData->GetUsedPointIndexForControlPoint(aznumeric_caster(controlPointIndex));
+ const int usedPointIndex = meshData->GetUsedPointIndexForControlPoint(meshData->GetControlPointIndex(aznumeric_caster(controlPointIndex)));
const size_t linkCount = skinData.get().GetLinkCount(controlPointIndex);
if (usedPointIndex < 0 || linkCount == 0)
diff --git a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_1000.dds b/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_1000.dds
deleted file mode 100644
index 872d7b71d4..0000000000
--- a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_1000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:f454e6505870d9159eaac1eb0c53751e45e803cf50bf94e5c5f51ba2232cebba
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_2000.dds b/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_2000.dds
deleted file mode 100644
index 97ed5efe4d..0000000000
--- a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_2000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:c1047fad9be53568fc471bdb5633445a030efaed6bf9b5e9d47abb09efb4d01e
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_3000.dds b/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_3000.dds
deleted file mode 100644
index 337e63a40c..0000000000
--- a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_3000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:11b5326877643b06a5687cf1470e388752296df64ce3abb69aa06f1933e0f3b8
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_4000.dds b/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_4000.dds
deleted file mode 100644
index 2e6b3a3eda..0000000000
--- a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_4000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:de54072f3eca1a1de6250b1585335a1aa6fa9e07e4e7ad00cd37ea5f809a303c
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_5000.dds b/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_5000.dds
deleted file mode 100644
index 4a80ce04e1..0000000000
--- a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_5000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:ac0f98198af41590052eff6550d34f05d0e3ca374bc59e7ece314be2380c210f
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_6000.dds b/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_6000.dds
deleted file mode 100644
index d7e0e74fca..0000000000
--- a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_6000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:be47d7c0a2a64b17925e51b65abba72dd22735ef7f3913e9aa389f8c31124ede
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_7000.dds b/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_7000.dds
deleted file mode 100644
index 391bd3ec9f..0000000000
--- a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_7000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:372359625eec486abbbe7f9ab438b51627939bfbd39002d136b6d6b9c61bbe1b
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_8000.dds b/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_8000.dds
deleted file mode 100644
index 418a7ee3ed..0000000000
--- a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_8000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:91ba2bd2504a10a199964b35014df14a4405cf8fd47955a6c4ef9ca4f300637d
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_9000.dds b/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_9000.dds
deleted file mode 100644
index 9c11c2fa43..0000000000
--- a/Tests/Atom/GoldenImages/Windows/amd/Baseviewer/BistroBenchmark/screenshot_bistro_9000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:7d8442967964bab77d02a572e6e7f8fcd158e62308b7e6340d4d56be13f7f455
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_1000.dds b/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_1000.dds
deleted file mode 100644
index f9a46c53c8..0000000000
--- a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_1000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:5bf373730d725a14b6833b7862c6268a93d8f9d847032a60cdc99ce14fea9dfe
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_2000.dds b/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_2000.dds
deleted file mode 100644
index 25cce25c63..0000000000
--- a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_2000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:181c31eb7fa068027d3e42c415006fd70d1fbeb0bfe373b3290aea6e3002d762
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_3000.dds b/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_3000.dds
deleted file mode 100644
index a1c5d4e1b8..0000000000
--- a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_3000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:efc4fbc90bfd01a1ba09ebd925b8379e67263b7156f37473dd13a91f346ed1c4
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_4000.dds b/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_4000.dds
deleted file mode 100644
index 4cafe4e613..0000000000
--- a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_4000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:b48137886ef2a38312eedbddb111367b3f3924ab5425b49f81a92ae7d4ea898e
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_5000.dds b/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_5000.dds
deleted file mode 100644
index bb74f06212..0000000000
--- a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_5000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:ea47fd8d68dab6ba54e45bfc50d84d380ec2f5b7e6915713dc9e9aa41423d4ea
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_6000.dds b/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_6000.dds
deleted file mode 100644
index 8bc6e3de17..0000000000
--- a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_6000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:9bd3fe0208cbee26cfcf00cba0a127c5938061b8bea2143d05e28fbc7f55d9ca
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_7000.dds b/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_7000.dds
deleted file mode 100644
index 4c286062d0..0000000000
--- a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_7000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:30f6aac28e74da435ad6c018b6a7e9b7cee1ad74ec5452778e56036a7d438ec9
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_8000.dds b/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_8000.dds
deleted file mode 100644
index 31023d437e..0000000000
--- a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_8000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:2628992a8c8774ff5e77152999009ba3cbb3b68ee79d53669dd16aebe978da27
-size 5946320
diff --git a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_9000.dds b/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_9000.dds
deleted file mode 100644
index a5f85d6b22..0000000000
--- a/Tests/Atom/GoldenImages/Windows/nvidia/Baseviewer/BistroBenchmark/screenshot_bistro_9000.dds
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:42faae0235f0c74bc0201dd8fc805a9c01f086b093026b654e207f83ea0f3f90
-size 5946320
diff --git a/Tests/Atom/__init__.py b/Tests/Atom/__init__.py
deleted file mode 100755
index 36d43bea05..0000000000
--- a/Tests/Atom/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# """
-# 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/Tests/Atom/image_comparison_utils.py b/Tests/Atom/image_comparison_utils.py
deleted file mode 100755
index 698901710a..0000000000
--- a/Tests/Atom/image_comparison_utils.py
+++ /dev/null
@@ -1,105 +0,0 @@
-# """
-# 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.
-
-# Utility functions for image comparison tests.
-# """
-# import test_tools.shared.images.qssim as qssim
-# import PythonMagick
-# import logging
-# import os
-# import shared.s3_utils
-# import platform
-
-
-####################################
-# Commented out due to need to shift to new LyTestTools, Python3 and new screenshot workflow
-# Don't merge to Mainline
-####################################
-
-
-# def create_image_path(screenshot, path, extension, golden):
- # """
- # Create image path from name, path and extension
- # From a specified path, create the path for the diff image
- # :param screenshot: path to the screenshot which needs to be saved
- # :param path: path to where screenshot should be saved
- # :param extension: extension of the diff image (.dds, .jpg)
- # :param golden: True or False whether screenshot is golden image or not
- # :return diff_full_path: path of the iamge to be saved
- # """
- # screenshot_name = os.path.basename(screenshot)
- # if golden:
- # diff_name = "{}_golden{}".format(screenshot_name.split('.')[:-1][0], extension)
- # else:
- # diff_name = "{}{}".format(screenshot_name.split('.')[:-1][0], extension)
- # diff_full_path = os.path.join(path, diff_name)
- # return diff_full_path
-
-
-# def convert_dds_to_jpg(image, path, golden):
- # """
- # Convert DDS to JPEG
- # :param image: DDS image to convert
- # :param path: path to where iamge will be saved
- # :return screenshotJPG_path: path to the newly JPEG-converted DDS image
- # """
- # # Convert image as JPEG for quick review
- # screenshotJPG_path = create_image_path(image, path, '.jpg', golden)
- # screenshot = PythonMagick.Image(image)
- # screenshot.quality(100)
- # screenshot.magick('JPEG')
- # screenshot.write(screenshotJPG_path)
- # return screenshotJPG_path
-
-
-# def compare_screenshot_to_golden_image(screenshot, golden_image, path, threshold=0.985):
- # """
- # Compare Screenshots to Golden Images
- # Function to compare a newly taken screenshot with the golden image
- # :param screenshot: path of the screenshot
- # :param golden_image: path of the golden image (in Perforce)
- # :param path: path to where the screenshot diff image will be saved
- # :param threshold: threshold for the image comparison test to fail/pass (optional)
- # :return failure_not_found: True or False whether screenshots are similar (due to threshold) or not
- # """
- # failure_not_found = True
- # logging.info("Comparing screenshot {}".format(screenshot))
- # # Calculating screenshots similarity
- # quaternion_similarity = qssim.qssim(screenshot, golden_image, diff_path = path)
- # # Converting original screenshots to jpg
- # convert_dds_to_jpg(screenshot, path, False)
- # convert_dds_to_jpg(golden_image, path, True)
- # # Checking if similarity index is bypassing the threshold
- # if (quaternion_similarity < threshold):
- # failure_not_found = False
- # logging.error("%s failed the image comparison with %s", screenshot, golden_image)
- # else:
- # logging.info("Comparison successful, screenshots are similar.")
- # return failure_not_found
-
-
-# def upload_screenshots_to_s3(folder_path, folder_name):
- # """
- # Uploading screenshots to certain s3 bucket
- # Will require certain credentials (from the IAM that has access to l-qa@amazon acc) on the machine to work
- # :param folder_path: full path to folder that needs to be uploaded to s3
- # :param folder_name: name of the folder to be uploaded to s3
- # :return: None
- # """
- # host_name = platform.uname()[1]
- # s3_folder_name = '_'.join([folder_name, host_name])
-
- # logging.info("Trying to create a folder on S3; bucket: ly.screenshot.automation.artifacts, folder: {}".format(s3_folder_name))
- # shared.s3_utils.create_folder_in_bucket('ly.screenshot.automation.artifacts', s3_folder_name)
-
- # for file in os.listdir(folder_path):
- # key = '{}/{}'.format(s3_folder_name, file)
- # shared.s3_utils.upload_to_bucket('ly.screenshot.automation.artifacts', '{}/{}'.format(folder_path, file), key)
-
diff --git a/Tests/Atom/windows/__init__.py b/Tests/Atom/windows/__init__.py
deleted file mode 100755
index 36d43bea05..0000000000
--- a/Tests/Atom/windows/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# """
-# 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/Tests/Atom/windows/atomsampleviewer_tests_stability.py b/Tests/Atom/windows/atomsampleviewer_tests_stability.py
deleted file mode 100755
index b480a7bc1a..0000000000
--- a/Tests/Atom/windows/atomsampleviewer_tests_stability.py
+++ /dev/null
@@ -1,91 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-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 is a file to test basic functionality of the Base Viewer executable
-"""
-
-import os
-import pytest
-import subprocess
-import time
-import re
-from ly_test_tools.environment.process_utils import kill_processes_named as kill_processes_named
-
-dev_dir = os.path.abspath(os.path.join(os.path.abspath(__file__), '..', '..', '..', '..'))
-bin_dir = 'Bin64vc141'
-
-
-def gather_sample_names():
- """
- Gathers the currently eligible samples from the output of a single run of baseviewer.exe (with no sample argument).
- For use in the fixture parameters.
- """
- viewer_dir = os.path.join(dev_dir, bin_dir)
- os.chdir(viewer_dir)
- process = subprocess.Popen(['BaseViewer.exe', '-timeout', '5'], stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
- out = process.communicate()[0]
- log = out.splitlines()
- samples = []
- for line in log:
- line = str(line)
- if "SampleComponentManager" in line and '-' not in line and 'Not Supported' not in line:
- line_regexp = re.search('\[.*\]', line)
- line = line_regexp.group(0)
- sample = line.replace('[', '').replace(']', '')
- samples.append(sample)
- kill_processes_named('AssetProcessor', ignore_extensions=True)
- if samples is not None:
- return samples
-
-
-@pytest.fixture
-def kill_AP(request):
- def teardown():
- kill_processes_named('AssetProcessor', ignore_extensions=True)
- request.addfinalizer(teardown)
-
-
-@pytest.mark.parametrize("samples", gather_sample_names())
-class TestBaseViewerExe(object):
-
- def test_OpenSampleLevel_CorrectFormat_ShouldPass(self, samples, kill_AP):
- """
- Opens the specific BaseViewer samples individually and verifies they're stable for a few seconds and then exit
- cleanly
- """
- viewer_dir = os.path.join(dev_dir, bin_dir)
- os.chdir(viewer_dir)
- return_code = subprocess.check_call(['BaseViewer.exe', '-sample', samples, '-timeout', '20'], timeout=30)
- assert return_code == 0, "Sample '{}' did not exit properly with code '{}'".format(samples, str(returncode))
-
- def test_OpenSampleLevel_NoErrors_ShouldPass(self, samples, kill_AP):
- """
- Opens the specific BaseViewer samples individually and verifies there are no errors in the output while running
- """
- viewer_dir = os.path.join(dev_dir, bin_dir)
- os.chdir(viewer_dir)
- output = subprocess.check_output(['BaseViewer.exe', '-sample', samples, '-timeout', '20'], timeout=30)
- log = output.splitlines()
- errors = []
- assertions = []
- for i in range(len(log)):
- line = str(log[i])
- surrounding_lines = str(log[i:i+3])
- if "Trace::Error" in line:
- errors.append(surrounding_lines)
- if "Trace::Assert" in line:
- assertions.append(surrounding_lines)
-
- assert len(errors) == 0, "Sample '{}' had the following errors when run: {}".format(samples, "\n".join(errors))
- assert len(assertions) == 0, "Sample '{}' had the following assertions when run: {}".format(samples, "\n".join(assertions))
-
diff --git a/Tests/Atom/windows/conftest.py b/Tests/Atom/windows/conftest.py
deleted file mode 100755
index e0766a4015..0000000000
--- a/Tests/Atom/windows/conftest.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# """
-# 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.
-
-# Conftest file for providing additional configuration for Screenshot Comparison tests
-# """
-
-
-
-####################################
-# Commented out due to need to shift to new LyTestTools, Python3 and new screenshot workflow
-# Don't merge to Mainline
-####################################
-
-
-# import pytest
-
-# def pytest_addoption(parser):
- # parser.addoption(
- # '--graphics_vendor', action='store', help='graphics vendor name: nvidia or amd', required=True
- # )
- # parser.addoption(
- # '--upload_results_to_s3', action='store_true', default=False, help='Specify if you need to upload screenshot artifacts to s3'
- # )
-
-# @pytest.fixture
-# def graphics_vendor(request):
- # return request.config.getoption('--graphics_vendor')
-
-# @pytest.fixture
-# def upload_results_to_s3(request):
- # return request.config.getoption('--upload_results_to_s3')
-
diff --git a/Tests/Atom/windows/screenshot_comparison_atomsampleviewer_windows.py b/Tests/Atom/windows/screenshot_comparison_atomsampleviewer_windows.py
deleted file mode 100755
index fca50c9901..0000000000
--- a/Tests/Atom/windows/screenshot_comparison_atomsampleviewer_windows.py
+++ /dev/null
@@ -1,143 +0,0 @@
-# """
-# 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.
-
-# BaseViewer image comparison tests on windows
-# """
-
-# import Atom.image_comparison_utils as image_comparison_utils
-# import test_tools.builtin.fixtures as fixtures
-# import subprocess
-# import pytest
-# import os
-# import logging
-# import time
-# import datetime
-# from test_tools import WINDOWS
-# from test_tools.shared.process_utils import kill_processes_named
-# from test_tools.shared.waiter import wait_for
-
-
-
-
-####################################
-# Commented out due to need to shift to new LyTestTools, Python3 and new screenshot workflow
-# Don't merge to Mainline
-####################################
-
-
-
-
-# workspace = fixtures.use_fixture(fixtures.builtin_workspace_fixture, scope='function')
-# logger = logging.getLogger(__name__)
-
-# @pytest.fixture(scope='session', autouse=True)
-# def closing_ap(request):
- # """
- # Fixture to call once per all tests to teardown AP at the end
- # :param request: pytest request
- # """
- # def teardown():
- # kill_processes_named('AssetProcessor_tmp', ignore_extensions=True)
- # kill_processes_named('AssetProcessor', ignore_extensions=True)
- # kill_processes_named('AssetProcessorBatch', ignore_extensions=True)
- # kill_processes_named('AssetBuilder', ignore_extensions=True)
- # kill_processes_named('rc', ignore_extensions=True)
- # request.addfinalizer(teardown)
-
-# @pytest.fixture()
-# def screenshots_setup(request, workspace, sample, graphics_vendor, upload_results_to_s3):
- # """
- # Fixture for setting up workspace needed for screenshot comparison test
- # :param request: pytest request
- # :param workspace: pythontesttools workspace object
- # :param sample: name of BaseViwer sample
- # :return final_path: path to folder where output screenshots will be stored
- # """
- # # Creating output folder
- # tests_path = os.path.dirname(os.path.realpath(__file__))
- # dir_name = "{}_screenshot_tests_{}_{}_{}_{}".format(datetime.datetime.now().strftime("%Y-%m-%d_%H_%M_%S_%f"), sample.replace('/', ""), workspace.release.platform, workspace.release.configuration,
- # graphics_vendor)
- # dir_name = dir_name.replace(":", '_')
- # final_path = os.path.join(tests_path, dir_name)
- # os.mkdir(final_path)
-
- # # Teardown to clean up Cache from .dds screenshots that are produced by BaseViewer.exe
- # def teardown():
- # cache = os.path.join(workspace.release.paths.dev(), "Cache", "BaseViewer", "pc", "baseviewer")
- # files = os.listdir(cache)
- # for file in files:
- # name, file_extension = os.path.splitext(file)
- # if 'screenshot' in name and file_extension == '.dds':
- # screen_to_remove = os.path.join(cache, file)
- # logger.info('Deleting temp screenshot file {}.'.format(screen_to_remove))
- # os.remove(screen_to_remove)
- # kill_processes_named('BaseViewer', ignore_extensions=True)
- # # uploading screenshots to s3
- # if upload_results_to_s3:
- # image_comparison_utils.upload_screenshots_to_s3(final_path, dir_name)
- # request.addfinalizer(teardown)
- # return final_path
-
-
-# # Commenting out debug due to ATOM-1677
-# @pytest.mark.parametrize("platform,configuration,project,spec,sample", [
- # pytest.param("win_x64_vs2017", "profile", "BaseViewer", "all", "RPI/BistroBenchmark",
- # marks=pytest.mark.skipif(not WINDOWS, reason="Only supported on Windows hosts")),
- # #pytest.param("win_x64_vs2017", "debug", "BaseViewer", "all", "RPI/BistroBenchmark",
- # # marks=pytest.mark.skipif(not WINDOWS, reason="Only supported on Windows hosts")),
- # ])
-# class TestBaseViewerScreenshots(object):
- # def test_BistroBenchmarkSample_CompareScreenshots(self, request, workspace, sample, screenshots_setup, graphics_vendor):
- # """
- # Launches BaseViewer.exe RPI/BistoBenchmark, taking screenshot and comparing on certain frames.
- # """
- # base_path = workspace.release.paths.dev()
- # # Generating frames list parameter
- # frames_list = range(1000,10000,1000)
- # frames_param = ""
- # screenshot_names = []
- # for parameter in frames_list:
- # frames_param += '{},'.format(parameter)
- # screenshot_names.append('screenshot_bistro_{}.dds'.format(parameter))
- # frames_param = frames_param[:-1]
- # # Loading BaseViewer
- # self.load_baseviewer_directly(workspace, sample, frames_param, timeout=100)
-
- # logger.info('Comparing screenshots to golden images')
- # taken_screens_path = os.path.join(base_path, "Cache", "BaseViewer", "pc", "baseviewer")
- # golden_screens_path = os.path.join(base_path, "Tests", "Atom", "GoldenImages", "Windows", graphics_vendor, "Baseviewer", "BistroBenchmark")
- # failed_screenshots = []
- # for screen in screenshot_names:
- # taken_image = os.path.join(taken_screens_path, screen)
- # golden_image = os.path.join(golden_screens_path, screen)
- # if not image_comparison_utils.compare_screenshot_to_golden_image(taken_image, golden_image, screenshots_setup):
- # failed_screenshots.append(screen)
- # if len(failed_screenshots) > 0:
- # assert False, "A failure has been found during image comparison for the following images: {}".format(failed_screenshots)
-
-
- # def load_baseviewer_directly(self, workspace, sample, frames, timeout):
- # """
- # Launch directly Baseviewer without using the launcher (since Atom is not yet integrated in Lumberyard)
- # :param workspace: pythontesttools workspace object
- # :param sample: name of the sample from BaseViewer
- # :param frames: list of frames to take screenshots at
- # :param timeout: time in seconds to wait BaseViewer to take screenshots
- # """
- # base_path = workspace.release.paths.dev()
- # bin_dir = workspace.release.paths.bin_dir()
- # cmd_path = os.path.join(base_path, bin_dir)
- # os.chdir(cmd_path)
- # p = subprocess.Popen(['BaseViewer.exe', '-sample', sample, '-screenshot', frames])
- # # Wait for BaseViewer to run and take screenshot
- # last_screenshot = frames.split(',')[-1]
- # screenshot_file = os.path.join(base_path, "Cache", "BaseViewer", "pc", "baseviewer", "screenshot_bistro_{}.dds".format(last_screenshot))
- # wait_for(lambda: os.path.exists(screenshot_file), timeout)
-
diff --git a/Tests/BuildSystems/test_BuildBAT.py b/Tests/BuildSystems/test_BuildBAT.py
deleted file mode 100755
index 1521508550..0000000000
--- a/Tests/BuildSystems/test_BuildBAT.py
+++ /dev/null
@@ -1,212 +0,0 @@
-"""
-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.
-
-BuildSystems BAT to automate building on packages for Windows
-"""
-import logging
-import pytest
-import os
-
-pytest.importorskip('ly_test_tools')
-
-import ly_test_tools.builtin.helpers as helpers
-from .test_lib import build_helper
-
-logger = logging.getLogger(__name__)
-
-
-@pytest.mark.BAT
-@pytest.mark.parametrize('spec', ['all'])
-@pytest.mark.parametrize('project', ['AutomatedTesting'])
-
-class TestWindowsBuildConfig(object):
- """
- Automated tests for all the build configurations for Windows.
- Test cases live in Repository/Build System/Lumberyard Builds/Configurations
- """
- @pytest.mark.test_case_id('C15723869')
- @pytest.mark.parametrize('platform', ['win_x64_vs2017'])
- @pytest.mark.parametrize('configuration', ['profile'])
- @pytest.mark.build
- def test_build_win_x64_vs2017_profile(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15723870')
- @pytest.mark.parametrize('platform', ['win_x64_vs2017'])
- @pytest.mark.parametrize('configuration', ['profile_dedicated'])
- @pytest.mark.build
- def test_build_win_x64_vs2017_profile_dedicated(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15723871')
- @pytest.mark.parametrize('platform', ['win_x64_vs2017'])
- @pytest.mark.parametrize('configuration', ['profile_test'])
- @pytest.mark.build
- def test_build_win_x64_vs2017_profile_test(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15723872')
- @pytest.mark.parametrize('platform', ['win_x64_vs2017'])
- @pytest.mark.parametrize('configuration', ['profile_test_dedicated'])
- @pytest.mark.build
- def test_build_win_x64_vs2017_profile_test_dedicated(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15716369')
- @pytest.mark.parametrize('platform', ['win_x64_vs2017'])
- @pytest.mark.parametrize('configuration', ['debug'])
- @pytest.mark.build
- def test_build_win_x64_vs2017_debug(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15716370')
- @pytest.mark.parametrize('platform', ['win_x64_vs2017'])
- @pytest.mark.parametrize('configuration', ['debug_dedicated'])
- @pytest.mark.build
- def test_build_win_x64_vs2017_debug_dedicated(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15716371')
- @pytest.mark.parametrize('platform', ['win_x64_vs2019'])
- @pytest.mark.parametrize('configuration', ['debug_test'])
- @pytest.mark.build
- def test_build_win_x64_vs2019_debug_test(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15716372')
- @pytest.mark.parametrize('platform', ['win_x64_vs2017'])
- @pytest.mark.parametrize('configuration', ['debug_test_dedicated'])
- @pytest.mark.build
- def test_build_win_x64_vs2017_debug_test_dedicated(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15723889')
- @pytest.mark.parametrize('platform', ['win_x64_vs2017'])
- @pytest.mark.parametrize('configuration', ['release'])
- @pytest.mark.build
- def test_build_win_x64_vs2017_release(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15723890')
- @pytest.mark.parametrize('platform', ['win_x64_vs2017'])
- @pytest.mark.parametrize('configuration', ['release_dedicated'])
- @pytest.mark.build
- def test_build_win_x64_vs2017_release_dedicated(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15815180')
- @pytest.mark.parametrize('platform', ['win_x64_vs2019'])
- @pytest.mark.parametrize('configuration', ['profile'])
- @pytest.mark.build
- def test_build_win_x64_vs2019_profile(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15815181')
- @pytest.mark.parametrize('platform', ['win_x64_vs2019'])
- @pytest.mark.parametrize('configuration', ['profile_dedicated'])
- @pytest.mark.build
- def test_build_win_x64_vs2019_profile_dedicated(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15815182')
- @pytest.mark.parametrize('platform', ['win_x64_vs2019'])
- @pytest.mark.parametrize('configuration', ['profile_test'])
- @pytest.mark.build
- def test_build_win_x64_vs2019_profile_test(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15815183')
- @pytest.mark.parametrize('platform', ['win_x64_vs2019'])
- @pytest.mark.parametrize('configuration', ['profile_test_dedicated'])
- @pytest.mark.build
- def test_build_win_x64_vs2019_profile_test_dedicated(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15815174')
- @pytest.mark.parametrize('platform', ['win_x64_vs2019'])
- @pytest.mark.parametrize('configuration', ['debug'])
- @pytest.mark.build
- def test_build_win_x64_vs2019_debug(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15815175')
- @pytest.mark.parametrize('platform', ['win_x64_vs2019'])
- @pytest.mark.parametrize('configuration', ['debug_dedicated'])
- @pytest.mark.build
- def test_build_win_x64_vs2019_debug_dedicated(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15815176')
- @pytest.mark.parametrize('platform', ['win_x64_vs2019'])
- @pytest.mark.parametrize('configuration', ['debug_test'])
- @pytest.mark.build
- def test_build_win_x64_vs2019_debug_test(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15815177')
- @pytest.mark.parametrize('platform', ['win_x64_vs2019'])
- @pytest.mark.parametrize('configuration', ['debug_test_dedicated'])
- @pytest.mark.build
- def test_build_win_x64_vs2019_debug_test_dedicated(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15815190')
- @pytest.mark.parametrize('platform', ['win_x64_vs2019'])
- @pytest.mark.parametrize('configuration', ['release'])
- @pytest.mark.build
- def test_build_win_x64_vs2019_release(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
-
- @pytest.mark.test_case_id('C15815191')
- @pytest.mark.parametrize('platform', ['win_x64_vs2019'])
- @pytest.mark.parametrize('configuration', ['release_dedicated'])
- @pytest.mark.build
- def test_build_win_x64_vs2019_release_dedicated(self, workspace, platform, configuration, project, spec):
- workspace.build()
- build_log = os.path.join(workspace.artifact_manager.dest_path, 'waf_build.log')
- assert build_helper.verify_build_log(build_log, platform, configuration)
\ No newline at end of file
diff --git a/Tests/BuildSystems/test_lib/build_helper.py b/Tests/BuildSystems/test_lib/build_helper.py
deleted file mode 100755
index c2213d139b..0000000000
--- a/Tests/BuildSystems/test_lib/build_helper.py
+++ /dev/null
@@ -1,38 +0,0 @@
-"""
-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.
-
-Helper functions for build systems
-"""
-import os
-import logging
-
-logger = logging.getLogger(__name__)
-
-
-def verify_build_log(build_log_path, platform, configuration):
- """
- This will search the log file for an expected success message for a specific platform configuration.
- :param build_log_path: the full path to the log file. e.g. \\dev\TestResults\timestamp_folder\pytest_results\...
- :param platform: the compiler to use, e.g. "win_x64_vs2017"
- :param configuration: the flavor of the build, e.g. "profile"
- :return: True, if success message is found within the build log. False, if success message is not found and raise an assertion error if the build log cannot be found.
- """
- success_message = "[WAF] 'build_{0}_{1}' finished successfully".format(platform, configuration)
- if os.path.exists(build_log_path):
- with open(build_log_path) as build_file:
- for line in build_file:
- if success_message in line:
- logger.info('Success message was found for {0}_{1}'.format(platform, configuration))
- return True
- logger.info('Success message not found for {0}_{1}'.format(platform, configuration))
- return False
- else:
- logger.info('We cannot find the build log and this is the path we are looking for {0}'.format(build_log_path))
- raise AssertionError
diff --git a/Tests/README.txt b/Tests/README.txt
deleted file mode 100644
index 71598c7890..0000000000
--- a/Tests/README.txt
+++ /dev/null
@@ -1 +0,0 @@
-This folder contains integration tests which do not ship with Lumberyard. Tests that ship with the product can be found in folders adjacent to the code that they test.
\ No newline at end of file
diff --git a/Tests/__init__.py b/Tests/__init__.py
deleted file mode 100755
index 6ed3dc4bda..0000000000
--- a/Tests/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""
-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/Tests/ai/EditorScripts/LY_114727_NavigationComponent_MovementMethods.py b/Tests/ai/EditorScripts/LY_114727_NavigationComponent_MovementMethods.py
deleted file mode 100755
index 75a13b8e17..0000000000
--- a/Tests/ai/EditorScripts/LY_114727_NavigationComponent_MovementMethods.py
+++ /dev/null
@@ -1,46 +0,0 @@
-#
-# 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 script tests movement methods Transform, Physics and Custom
-'''
-import sys, os
-import time
-import azlmbr.legacy.general as general
-
-from tests_common import TestHelper
-
-class TestMovementMethods(TestHelper):
- def __init__(self):
- TestHelper.__init__(self, log_prefix = 'LY-114727', args=['level'])
-
- def run_test(self):
- # Start by assuming we'll crash and fail
- self.test_success = False
- # Open the level non-interactively
- level_opened = self.open_level(self.get_arg('level'))
- if not level_opened:
- return
-
- # Enter game mode, so that physics in the test level starts running.
- general.enter_game_mode()
- # Wait for game mode to start. (Not sure if this is necessary, just being extra-cautious)
- while (general.is_in_game_mode() != True):
- general.idle_wait(1.0)
- # Wait for game mode to finish. Entities should be navigating
- while (general.is_in_game_mode() == True):
- general.idle_wait(2.0)
-
- # We finished and haven't crashed, so assume success and exit the Editor.
- self.test_success = True
-
-test = TestMovementMethods()
-test.run()
-
diff --git a/Tests/ai/EditorScripts/tests_common.py b/Tests/ai/EditorScripts/tests_common.py
deleted file mode 100755
index 76ad183c60..0000000000
--- a/Tests/ai/EditorScripts/tests_common.py
+++ /dev/null
@@ -1,163 +0,0 @@
-#
-# 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
-import azlmbr.legacy.general as general
-import azlmbr.legacy.settings as settings
-
-class TestHelper:
- def __init__(self, log_prefix, args = None):
- self.log_prefix = log_prefix + ': '
- self.test_success = True
- self.args = {}
- if args:
- # Get the level name and heightmap name from command-line args
- if (len(sys.argv) == (len(args) + 1)):
- for arg_index in range(len(args)):
- self.args[args[arg_index]] = sys.argv[arg_index + 1]
- else:
- test_success = False
- self.log('Expected command-line args: {}'.format(args))
-
-
- # Test Setup
- # Set helpers
- # Set viewport size
- # Turn off display mode, antialiasing
- # set log prefix, log test started
- # TODO: Turn off user dialogs like Amazon login, surveys, etc...
- def setup(self):
- self.log('test started')
-
- def after_level_load(self):
- # Enable the Editor to start running its idle loop.
- # This is needed for Python scripts passed into the Editor startup. Since they're executed
- # during the startup flow, they run before idle processing starts. Without this, the engine loop
- # won't run during idle_wait, which will prevent our test level from working.
- general.idle_enable(True)
-
- # Give everything a second to initialize
- general.idle_wait(1.0)
-
- self.original_settings = settings.get_misc_editor_settings()
- self.helpers_visible = general.is_helpers_shown()
- self.viewport_size = general.get_viewport_size()
- # Turn off the helper gizmos if visible
- if (self.helpers_visible):
- general.toggle_helpers()
- general.idle_wait(1.0)
-
- # Set Editor viewport to a well-defined size
- general.set_viewport_size(1280, 720)
- general.idle_wait(1.0)
-
- # Turn off any display info like FPS, as that will mess up our image comparisons
- # Turn off antialiasing as well
- general.run_console("r_displayInfo=0")
- general.run_console("r_antialiasingmode=0")
- general.idle_wait(1.0)
-
-
-
- # Test Teardown
- # Restore everything from above
- # log test results, exit editor
- def teardown(self):
- # Restore the original Editor settings
- settings.set_misc_editor_settings(self.original_settings)
- # If the helper gizmos were on at the start, restore them
- if (self.helpers_visible):
- general.toggle_helpers()
- # Set the viewport back to whatever size it was at the start
- general.set_viewport_size(self.viewport_size.x, self.viewport_size.y)
- general.idle_wait(1.0)
-
- self.log('test finished')
-
- if self.test_success == True:
- self.log('result=SUCCESS')
- general.set_result_to_success()
- else:
- self.log('result=FAILURE')
- general.set_result_to_failure()
-
- general.exit_no_prompt()
-
- def run_test(self):
- self.log('run')
-
- def run(self):
- self.setup()
-
- # Only run the actual test if we didn't have setup issues
- if self.test_success:
- self.run_test()
-
- self.teardown()
-
- def get_arg(self, arg_name):
- if arg_name in self.args:
- return self.args[arg_name]
- return ''
-
-
- # general logger that adds prefix?
- def log(self, log_line):
- general.log(self.log_prefix + log_line)
-
- # isclose: Compares two floating-point values for "nearly-equal"
- # From https://www.python.org/dev/peps/pep-0485/#proposed-implementation :
- def isclose(self, a, b, rel_tol=1e-9, abs_tol=0.0):
- return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
-
-
- # Create a new empty level
- def create_level(self, level_name, heightmap_resolution, heightmap_meters_per_pixel, terrain_texture_resolution, use_terrain):
- self.log('Creating level {}'.format(level_name))
- result = general.create_level_no_prompt(level_name, heightmap_resolution, heightmap_meters_per_pixel, terrain_texture_resolution, use_terrain)
-
- # Result codes are ECreateLevelResult defined in CryEdit.h
- if (result == 1):
- self.log('Temp level already exists')
- elif (result == 2):
- self.log('Failed to create directory')
- elif (result == 3):
- self.log('Directory length is too long')
- elif (result != 0):
- self.log('Unknown error, failed to create level')
- else:
- self.log('Level created successfully')
- self.after_level_load()
-
- return (result == 0)
-
- def open_level(self, level_name):
- # Open the level non-interactively
- self.log('Opening level {}'.format(level_name))
- result = general.open_level_no_prompt(level_name)
- self.after_level_load()
- if result:
- self.log('Level opened successfully')
- else:
- self.log('Unknown error, level failed to open')
-
- return result
-
- # Take Screenshot
- def take_screenshot(self, posX, posY, posZ, rotX, rotY, rotZ):
- # Set our camera position / rotation and wait for the Editor to acknowledge it
- general.set_current_view_position(posX, posY, posZ)
- general.set_current_view_rotation(rotX, rotY, rotZ)
- general.idle_wait(1.0)
- # Request a screenshot and wait for the Editor to process it
- general.run_console("r_GetScreenShot=2")
- general.idle_wait(1.0)
-
diff --git a/Tests/ai/LY_114727_NavigationComponent_test.py b/Tests/ai/LY_114727_NavigationComponent_test.py
deleted file mode 100755
index 3eff39c787..0000000000
--- a/Tests/ai/LY_114727_NavigationComponent_test.py
+++ /dev/null
@@ -1,70 +0,0 @@
-"""
-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.
-"""
-
-
-"""
-ly102242: Runs the ly-102242 level in the Editor which reproduces the appropriate steps for
-bug ly-102242 to crash when spawning an invalid touchbending asset. The touchbending asset
-can be made invalid by including multiple meshes - one with skinning data and one without.
-NOTE: In the bugfixed case, a crash will not occur, but touchbending will not occur and errors
-will be printed to the console every time a new asset gets "touched" (spawned in the physics system).
-"""
-import pytest
-pytest.importorskip('ly_test_tools')
-import logging
-import os
-
-from ..ly_shared import hydra_lytt_test_utils as hydra_utils
-
-logger = logging.getLogger(__name__)
-test_directory = os.path.join(os.path.dirname(__file__), 'EditorScripts')
-editor_timeout = 30
-
-@pytest.mark.parametrize('platform', ['win_x64_vs2017'])
-@pytest.mark.parametrize('configuration', ['profile'])
-@pytest.mark.parametrize('project', ['AutomatedTesting'])
-@pytest.mark.parametrize('spec', ['all'])
-@pytest.mark.parametrize('level', ['AI/NavigationComponentTest'])
-class TestNavigationComponent(object):
- @pytest.fixture(autouse=True)
- def setup_teardown(self, request):
- def teardown():
- if hasattr(self, 'cfg_file_name'):
- hydra_utils.cleanup_cfg_file(self.cfg_file_name)
-
- # Setup - add the teardown finalizer
- request.addfinalizer(teardown)
-
- # entities with Transform, Physics and Custom movement methods navigate to the goal
- def test_NavigationComponent(self, request, legacy_editor, level):
-
- cfg_args = [level]
-
- expected_lines = [
- "OnActivate NavigationAgentCustom",
- "OnActivate NavigationAgentPhysics",
- "OnActivate NavigationAgentTransform",
-
- "OnTraversalComplete NavigationAgentCustom",
- "OnTraversalComplete NavigationAgentPhysics",
- "OnTraversalComplete NavigationAgentTransform",
- ]
-
- unexpected_lines = [
- "OnTraversalCanceled NavigationAgentCustom",
- "OnTraversalCanceled NavigationAgentPhysics",
- "OnTraversalCanceled NavigationAgentTransform",
- ]
-
- hydra_utils.launch_and_validate_results(request, test_directory, legacy_editor,
- 'LY_114727_NavigationComponent_MovementMethods.py',
- expected_lines, unexpected_lines, timeout=editor_timeout, cfg_args=cfg_args)
-
diff --git a/Tests/ai/__init__.py b/Tests/ai/__init__.py
deleted file mode 100755
index e912252f4e..0000000000
--- a/Tests/ai/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""
-
- 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/Tests/demos/__init__.py b/Tests/demos/__init__.py
deleted file mode 100755
index e912252f4e..0000000000
--- a/Tests/demos/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""
-
- 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/Tests/demos/launcher_loading_tests.py b/Tests/demos/launcher_loading_tests.py
deleted file mode 100755
index 6d23f56f1b..0000000000
--- a/Tests/demos/launcher_loading_tests.py
+++ /dev/null
@@ -1,183 +0,0 @@
-"""
-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 pytest
-import ly_test_tools
-
-import os
-import shutil
-import subprocess
-from ly_test_tools.builtin.helpers import *
-import tempfile
-import time
-# The following imports are used to detect capabilities in the current system
-
-from ly_test_tools.environment.process_utils import *
-import Tests.shared.asset_processor_utils as aputil
-
-# Built-in fixture: provides a ready to use workspace.
-from Tests.shared import substring
-from ly_test_tools.environment.waiter import wait_for
-
-LAUNCHER_TIMEOUT = 120
-
-
-@pytest.mark.system
-class TestProjectLauncher:
- @pytest.fixture(autouse=True)
- def setup_teardown(self, request, workspace, launcher):
- path_to_cache = workspace.paths.asset_cache()
- # Remove previous artifacts from cache
- if os.path.isdir(os.path.join(path_to_cache, "user")):
- shutil.rmtree(os.path.join(path_to_cache, "user"))
-
- user_cfg_path = os.path.join(workspace.paths.dev(), "user.cfg")
- cfg_staging_path = None
-
- # Backup user.cfg if one exists
- if os.path.exists(user_cfg_path):
- cfg_staging_path = os.path.join(tempfile.gettempdir(), "user.cfg")
- shutil.move(user_cfg_path, cfg_staging_path)
-
- # Configure the headless client
- with open(user_cfg_path, "w") as user_cfg:
- user_cfg.write("r_driver=NULL\n")
- user_cfg.write("sys_audio_disable=1\n")
- user_cfg.write("sys_skip_input=1\n")
-
- def teardown():
- launcher.kill()
-
- aputil.kill_asset_processor()
-
- # Restore previous user.cfg if one existed and unconfigure the headless client
- if cfg_staging_path:
- shutil.move(os.path.join(tempfile.gettempdir(), "user.cfg"), user_cfg_path)
- elif os.path.exists(user_cfg_path):
- os.remove(user_cfg_path)
-
- # save logs,screenshots
- if os.path.exists(workspace.paths.project_log()):
- workspace.artifact_manager.save_artifact(workspace.paths.project_log())
- if os.path.isdir(workspace.paths.project_screenshots()):
- workspace.artifact_manager.save_artifact(workspace.paths.project_screenshots())
-
- request.addfinalizer(teardown)
-
- @pytest.mark.parametrize('level', ['Samples/Fur_Technical_Sample', 'Samples/Advanced_RinLocomotion', 'UI/UiFeatures','Samples/Simple_JackLocomotion',
- 'Samples/ScriptedEntityTweenerSample/SampleFullscreenAnimation', 'UI/UiMainMenuLuaSample', 'Samples/Metastream_Sample',
- 'Samples/ScriptCanvas_Sample/ScriptCanvas_Basic_Sample', 'UI/UiIn3DWorld', 'Samples/Audio_Sample'])
- @pytest.mark.test_case_id('C1698289,C1698287,C1698294,C1698280,C1698295,C1698293,C1698289,C1698290,C1698292,C1698288')
- @pytest.mark.parametrize('platform', ['win_x64_vs2017', 'win_x64_vs2019'])
- @pytest.mark.parametrize('configuration', ['profile'])
- @pytest.mark.parametrize('project', ['SamplesProject'])
- @pytest.mark.parametrize('spec', ['all'])
- def test_LaunchAndWait_LoadsLevelAndQuit_NoCrash(self, workspace, level, launcher):
- """
- Launch the Project Launcher and sets the specified level.
- Performs the console steps by passing in args.
- Loads the specified level and quit's the launcher.
- """
- # Fast fail if the level doesn't exist
- assert os.path.exists(os.path.join(workspace.paths.project(), "Levels", level)) and os.path.isdir(
- os.path.join(workspace.paths.project(), "Levels", level)), "Level Doesn't Exist"
-
- launcher.args = ["+map", level]
- launcher.launch()
-
- pattern_string = "Loading level " + level
- test = os.path.join(workspace.paths.project_log(), "Game.log")
- wait_for(lambda: os.path.exists(os.path.join(workspace.paths.project_log(), "Game.log")))
- wait_for(lambda: substring.in_file(
- os.path.join(workspace.paths.project_log(), "Game.log"), pattern_string), LAUNCHER_TIMEOUT)
- assert not os.path.exists(
- os.path.join(workspace.paths.project_log(), "error.log")), "Launcher Crashed Unexpectedly"
-
- # This is a convenient place to also verify LY-90255 for free here
- # (as well as any other text that must appear in the log).
- # writing a separate test to launch the launcher and examine the log would just waste time as the
- # above test already launches the launcher, and waits for it to finish anyway.
-
- assert substring.in_file(os.path.join(workspace.paths.project_log(), "Game.log"), "Initializing CryFont done, MemUsage")
-
-
-
- @pytest.mark.parametrize('level',['UI/UiFeatures','Samples/Metastream_Sample'])
- @pytest.mark.parametrize('platform', ['win_x64_vs2017', 'win_x64_vs2019'])
- @pytest.mark.parametrize('configuration', ['profile'])
- @pytest.mark.parametrize('project', ['SamplesProject'])
- @pytest.mark.parametrize('spec', ['all'])
- def test_LaunchAndWait_LoadsLevelFromPakAndQuit_NoCrash(self, workspace, level, launcher):
- """
- This test ensure that the Launcher can load levels from inside paks
- """
-
- if workspace.platform == 'win_x64_vs2017':
- binDirPath = os.path.join(workspace.paths._dev_path, 'Bin64vc141')
- if workspace.platform == 'win_x64_vs2019':
- binDirPath = os.path.join(workspace.paths._dev_path, 'Bin64vc142')
-
- # Launch APBatch so that it can process all assets and quit
- subprocess.check_call(
- [os.path.join(binDirPath, 'AssetProcessorBatch'), "/gamefolder=SamplesProject"])
-
- lowercaseProjectName = workspace.project.lower()
- cacheLevelDir = os.path.join(workspace.paths.platform_cache(), lowercaseProjectName, "levels")
- cacheTempLevelDir = os.path.join(workspace.paths.platform_cache(), lowercaseProjectName, "templevels")
-
- # Rename levels dir so that runtime cannot load levels from it
- os.rename(cacheLevelDir, cacheTempLevelDir)
- # Make an empty levels folder
- os.mkdir(cacheLevelDir)
-
- # make an archive of all the levels
- outputLevelsArchive = os.path.join(cacheLevelDir, "templevels")
- shutil.make_archive(outputLevelsArchive, 'zip', cacheTempLevelDir)
- # make archive will make a zip file
- outputLevelsArchive = outputLevelsArchive + ".zip"
-
- # change extension from zip to pak
- baseFileName = os.path.splitext(outputLevelsArchive)[0]
- os.rename(outputLevelsArchive, baseFileName + ".pak")
-
- # Launching AP again for SamplesProject gameproject
- subprocess.Popen([os.path.join(binDirPath, 'AssetProcessor'), "/gamefolder=SamplesProject", "--zeroAnalysisMode"])
-
- # Waiting to give AP time some time to start the listening thread otherwise the launcher will try to launch another instance of AP
- time.sleep(1)
-
- # ensure that in the cache level does not exist on disk
- assert not os.path.exists(os.path.join(cacheLevelDir, level))
-
- # load the levels
- launcher.args = ["+map", level]
- launcher.launch()
-
- pattern_string = "Loading level " + level
- wait_for(lambda: os.path.exists(os.path.join(workspace.paths.project_log(), "Game.log")))
- wait_for(lambda: substring.in_file(
- os.path.join(workspace.paths.project_log(), "Game.log"), pattern_string), LAUNCHER_TIMEOUT)
- assert not os.path.exists(
- os.path.join(workspace.paths.project_log(), "error.log")), "Launcher Crashed Unexpectedly"
-
- loaded_level_pattern = "Level " + level + " loaded"
- wait_for(lambda: substring.in_file(
- os.path.join(workspace.paths.project_log(), "Game.log"), loaded_level_pattern), LAUNCHER_TIMEOUT)
-
- launcher.stop()
-
- aputil.kill_asset_processor()
-
- shutil.rmtree(cacheLevelDir)
- os.rename(cacheTempLevelDir, cacheLevelDir)
-
-
-
diff --git a/Tests/demos/mac/__init__.py b/Tests/demos/mac/__init__.py
deleted file mode 100755
index e912252f4e..0000000000
--- a/Tests/demos/mac/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""
-
- 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/Tests/demos/mac/demos_mac.py b/Tests/demos/mac/demos_mac.py
deleted file mode 100755
index a2a44eab20..0000000000
--- a/Tests/demos/mac/demos_mac.py
+++ /dev/null
@@ -1,142 +0,0 @@
-"""
-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.
-
-These tests will validate that StarterGame and SamplesProject can be setup and built, have no failing assets processed,
-and will then have a screen shot taken to verify that it renders normally. All the logs and screenshots will be
-transferred to the test results to be zipped up and added to the Flume result. These projects will be run in profile
-and debug.
-"""
-import logging
-import os
-import pytest
-
-from demos.test_lib.demos_testlib import load_level, remote_console_take_screenshot, start_launcher, start_remote_console
-import shared.shader_compile_server_utils as compile_server
-import test_tools.shared.file_utils as file_utils
-from test_tools.shared.launcher_testlib import configure_setup, assert_build_success, assert_process_assets
-
-import test_tools.builtin.fixtures as fixtures
-from test_tools import MAC_LAUNCHER
-import test_tools.launchers.phase
-from test_tools.shared.remote_console_commands import RemoteConsole
-
-logger = logging.getLogger(__name__)
-
-workspace = fixtures.use_fixture(fixtures.builtin_workspace_fixture, scope='function')
-
-
-@pytest.fixture
-def launcher_instance(request, workspace, level):
- """
- Creates a launcher fixture instance with an extra teardown for error log grabbing.
- """
- def teardown():
- """
- Tries to grab any error logs before moving on to the next test.
- """
- compile_server.stop_shader_compile_server()
-
- if os.path.exists(launcher.workspace.release.paths.project_log()):
- for file_name in os.listdir(launcher.workspace.release.paths.project_log()):
- file_utils.move_file(launcher.workspace.release.paths.project_log(),
- launcher.workspace.artifact_manager.get_save_artifact_path(),
- file_name)
-
- logs_exist = lambda: file_utils.gather_error_logs(
- launcher.workspace.release.paths.dev(),
- launcher.workspace.artifact_manager.get_save_artifact_path())
- try:
- test_tools.shared.waiter.wait_for(logs_exist)
- except AssertionError:
- print("No error logs found. Completing test...")
-
- request.addfinalizer(teardown)
-
- launcher = fixtures.launcher(request, workspace, level)
- return launcher
-
-
-@pytest.fixture
-def remote_console_instance(request):
- """
- Creates a remote console instance to send console commands.
- """
- console = RemoteConsole()
-
- def teardown():
- try:
- console.stop()
- except:
- pass
-
- request.addfinalizer(teardown)
-
- return console
-
-
-@pytest.mark.parametrize("platform,configuration,project,spec,level", [
- pytest.param("darwin_x64", "profile", "StarterGame", "all", "StarterGame",
- marks=pytest.mark.skipif(not MAC_LAUNCHER, reason="Only supported on Mac hosts")),
- pytest.param("darwin_x64", "debug", "StarterGame", "all", "StarterGame",
- marks=pytest.mark.skipif(not MAC_LAUNCHER, reason="Only supported on Mac hosts")),
- ])
-class TestSingleLevel(object):
- def test_single_level(self, launcher_instance, configuration, level, remote_console_instance):
- """
- Verifies projects with a given demo-level can compile and successfully launch.
- """
- configure_setup(launcher_instance)
-
- assert_build_success(launcher_instance)
- assert_process_assets(launcher_instance)
-
- compile_server.start_mac_shader_compile_server(os.path.join(launcher_instance.workspace.release.paths.dev(),
- "Tools"), configuration)
- start_launcher(launcher_instance)
- start_remote_console(launcher_instance, remote_console_instance)
-
- load_level(launcher_instance, remote_console_instance, level)
- remote_console_take_screenshot(launcher_instance, remote_console_instance, level)
-
-
-@pytest.mark.parametrize("platform,configuration,project,spec,level,levels", [
- pytest.param("darwin_x64", "profile", "SamplesProject", "all", "Advanced_RinLocomotion",
- ["Advanced_RinLocomotion", "Audio_Sample", "Fur_Technical_Sample",
- "Gems_InAppPurchases_Sample", "Metastream_Sample", "ScriptCanvas_Basic_Sample",
- "Simple_JackLocomotion", "SampleFullscreenAnimation", "UiFeatures", "UiIn3DWorld",
- "UiMainMenuLuaSample", "UiMainMenuScriptCanvasSample"],
- marks=pytest.mark.skipif(not MAC_LAUNCHER, reason="Only supported on Mac hosts")),
- pytest.param("darwin_x64", "debug", "SamplesProject", "all", "Advanced_RinLocomotion",
- ["Advanced_RinLocomotion", "Audio_Sample", "Fur_Technical_Sample",
- "Gems_InAppPurchases_Sample", "Metastream_Sample", "ScriptCanvas_Basic_Sample",
- "Simple_JackLocomotion", "SampleFullscreenAnimation", "UiFeatures", "UiIn3DWorld",
- "UiMainMenuLuaSample", "UiMainMenuScriptCanvasSample"],
- marks=pytest.mark.skipif(not MAC_LAUNCHER, reason="Only supported on Mac hosts")),
- ])
-# Testing for projects with multiple demo levels
-class TestMultipleLevels(object):
- """
- Verifies projects with multiple demo-levels can compile and successfully launch.
- """
- def test_multiple_levels(self, launcher_instance, configuration, levels, remote_console_instance):
- configure_setup(launcher_instance)
-
- assert_build_success(launcher_instance)
- assert_process_assets(launcher_instance)
-
- compile_server.start_mac_shader_compile_server(os.path.join(launcher_instance.workspace.release.paths.dev(),
- "Tools"), configuration)
- start_launcher(launcher_instance)
- start_remote_console(launcher_instance, remote_console_instance)
-
- # Switch to each level, check if it loads and takes a screen shot, then move to test folder for Flume
- for level in levels:
- load_level(launcher_instance, remote_console_instance, level)
- remote_console_take_screenshot(launcher_instance, remote_console_instance, level)
diff --git a/Tests/demos/test_lib/__init__.py b/Tests/demos/test_lib/__init__.py
deleted file mode 100755
index e912252f4e..0000000000
--- a/Tests/demos/test_lib/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""
-
- 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/Tests/demos/test_lib/demos_testlib.py b/Tests/demos/test_lib/demos_testlib.py
deleted file mode 100755
index 46ed627cab..0000000000
--- a/Tests/demos/test_lib/demos_testlib.py
+++ /dev/null
@@ -1,70 +0,0 @@
-"""
-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 demos_testlib file is used for a collection of reusable functionality that QA will use in their scripts specific
-to the setup of demo level tests.
-"""
-import sys
-
-import shared.network_utils as network_utils
-from shared.screenshot_utils import move_screenshots, take_screenshot_with_retries
-
-from test_tools.shared.launcher_testlib import *
-
-import test_tools.shared.waiter
-
-
-def start_launcher(launcher):
- """
- For PC: Used to start launcher and give time to load.
- """
- launcher.launch()
- launcher.run(test_tools.launchers.phase.TimePhase(120, 120))
-
-
-def load_level(launcher, remote_console, level):
- """
- Uses the remote console to use the map command to load a level and checks the console output for a successful load.
- """
- command = 'map {}'.format(level)
- load = remote_console.expect_log_line('LEVEL_LOAD_COMPLETE', 300)
- retry_console_command(remote_console, command, "Executing console command '{}'".format(command))
- assert load(), "{} level failed to load.".format(level)
-
- # Allow one minute to let level fully render and to test for stability
- launcher.run(test_tools.launchers.phase.TimePhase(60, 60))
-
-
-def start_remote_console(launcher, remote_console, on_devkit=False):
- """
- Starts the remote console. Used in QA scripts that require the use of remote console.
- """
- if on_devkit:
- test_tools.shared.waiter.wait_for(lambda: network_utils.check_for_remote_listening_port(4600, launcher.ip),
- timeout=600, exc=AssertionError('Port 4600 not listening.'))
- else:
- test_tools.shared.waiter.wait_for(lambda: network_utils.check_for_listening_port(4600), timeout=300,
- exc=AssertionError('Port 4600 not listening.'))
-
- remote_console.start()
-
- # Allows remote console time to connect to launcher.
- launcher.run(test_tools.launchers.phase.TimePhase(60, 60))
-
-
-def remote_console_take_screenshot(launcher, remote_console, level):
- """
- Uses the remote console to run the r_GetScreenshot command to take a screenshot of the current launcher and move
- the screenshot to the test results location.
- """
- screenshot_path = os.path.join(launcher.workspace.release.paths.platform_cache(), "user", "screenshots")
- take_screenshot_with_retries(remote_console, launcher, level)
- if os.path.exists(screenshot_path):
- move_screenshots(screenshot_path, '.jpg', launcher.workspace.artifact_manager.get_save_artifact_path())
diff --git a/Tests/demos/win/__init__.py b/Tests/demos/win/__init__.py
deleted file mode 100755
index 4d5680a30d..0000000000
--- a/Tests/demos/win/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-#
-# 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/Tests/demos/win/demos_pc.py b/Tests/demos/win/demos_pc.py
deleted file mode 100755
index cb8b9a0e4b..0000000000
--- a/Tests/demos/win/demos_pc.py
+++ /dev/null
@@ -1,151 +0,0 @@
-"""
-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.
-
-These tests will validate that each project can be setup and built, have no failing assets processed, and will then
-have a screenshot taken to verify that it renders normally. All the logs and screenshots will be transferred to the
-test results to be zipped up and added to the Flume result. These projects will be run in profile and debug.
-Currently SearchForEden and Bistro are failing and are temporarily commented out of these tests.
-"""
-import logging
-import os
-import pytest
-
-from demos.test_lib.demos_testlib import load_level, remote_console_take_screenshot, start_launcher, start_remote_console
-from test_tools.shared.file_utils import gather_error_logs, move_file
-from test_tools.shared.launcher_testlib import configure_setup, assert_build_success, assert_process_assets
-
-from test_tools import WINDOWS_LAUNCHER
-import test_tools.builtin.fixtures as fixtures
-import test_tools.launchers.phase
-from test_tools.shared.remote_console_commands import RemoteConsole
-
-logger = logging.getLogger(__name__)
-
-# use_fixture registers the imported fixture in pytest at the specified scope. The test should provide all the
-# parameters in the fixture's signature
-workspace = fixtures.use_fixture(fixtures.builtin_workspace_fixture, scope='function')
-
-
-@pytest.fixture
-def launcher_instance(request, workspace, level):
- """
- Creates a launcher fixture instance with an extra teardown for error log grabbing.
- """
- def teardown():
- """
- Tries to grab any error logs before moving on to the next test.
- """
- if os.path.exists(launcher.workspace.release.paths.project_log()):
- for file_name in os.listdir(launcher.workspace.release.paths.project_log()):
- move_file(launcher.workspace.release.paths.project_log(),
- launcher.workspace.artifact_manager.get_save_artifact_path(),
- file_name)
-
- logs_exist = lambda: gather_error_logs(
- launcher.workspace.release.paths.dev(),
- launcher.workspace.artifact_manager.get_save_artifact_path())
- try:
- test_tools.shared.waiter.wait_for(logs_exist)
- except AssertionError:
- print("No error logs found. Completing test...")
-
- request.addfinalizer(teardown)
-
- launcher = fixtures.launcher(request, workspace, level)
- return launcher
-
-
-@pytest.fixture
-def remote_console_instance(request):
- """
- Creates a remote console instance to send console commands.
- """
- console = RemoteConsole()
-
- def teardown():
- try:
- console.stop()
- except:
- pass
-
- request.addfinalizer(teardown)
-
- return console
-
-
-@pytest.mark.parametrize("platform,configuration,project,spec,level", [
- pytest.param("win_x64_vs2017", "profile", "StarterGame", "all", "StarterGame",
- marks=pytest.mark.skipif(not WINDOWS_LAUNCHER, reason="Only supported on Windows hosts")),
- pytest.param("win_x64_vs2019", "profile", "StarterGame", "all", "StarterGame",
- marks=pytest.mark.skipif(not WINDOWS_LAUNCHER, reason="Only supported on Windows hosts")),
- pytest.param("win_x64_vs2017", "debug", "StarterGame", "all", "StarterGame",
- marks=pytest.mark.skipif(not WINDOWS_LAUNCHER, reason="Only supported on Windows hosts")),
- pytest.param("win_x64_vs2019", "debug", "StarterGame", "all", "StarterGame",
- marks=pytest.mark.skipif(not WINDOWS_LAUNCHER, reason="Only supported on Windows hosts")),
- ])
-class TestSingleLevel(object):
- def test_single_level(self, launcher_instance, level, remote_console_instance):
- """
- Verifies projects with a given demo-level can compile and successfully launch.
- """
- configure_setup(launcher_instance)
-
- assert_build_success(launcher_instance)
- assert_process_assets(launcher_instance)
-
- start_launcher(launcher_instance)
- start_remote_console(launcher_instance, remote_console_instance)
-
- load_level(launcher_instance, remote_console_instance, level)
- remote_console_take_screenshot(launcher_instance, remote_console_instance, level)
-
-
-@pytest.mark.parametrize("platform,configuration,project,spec,level,levels", [
- pytest.param("win_x64_vs2017", "profile", "SamplesProject", "all", "Advanced_RinLocomotion",
- ["Advanced_RinLocomotion", "Audio_Sample", "Fur_Technical_Sample", "Gems_InAppPurchases_Sample",
- "Metastream_Sample", "ScriptCanvas_Basic_Sample", "Simple_JackLocomotion",
- "SampleFullscreenAnimation", "UiDrawCallsSample", "UiFeatures", "UiIn3DWorld", "UiMainMenuLuaSample",
- "UiMainMenuScriptCanvasSample", "UiTextureAtlasSample"],
- marks=pytest.mark.skipif(not WINDOWS_LAUNCHER, reason="Only supported on Windows hosts")),
- pytest.param("win_x64_vs2019", "profile", "SamplesProject", "all", "Advanced_RinLocomotion",
- ["Advanced_RinLocomotion", "Audio_Sample", "Fur_Technical_Sample", "Gems_InAppPurchases_Sample",
- "Metastream_Sample", "ScriptCanvas_Basic_Sample", "Simple_JackLocomotion",
- "SampleFullscreenAnimation", "UiDrawCallsSample", "UiFeatures", "UiIn3DWorld", "UiMainMenuLuaSample",
- "UiMainMenuScriptCanvasSample", "UiTextureAtlasSample"],
- marks=pytest.mark.skipif(not WINDOWS_LAUNCHER, reason="Only supported on Windows hosts")),
- pytest.param("win_x64_vs2017", "debug", "SamplesProject", "all", "Advanced_RinLocomotion",
- ["Advanced_RinLocomotion", "Audio_Sample", "Fur_Technical_Sample", "Gems_InAppPurchases_Sample",
- "Metastream_Sample", "ScriptCanvas_Basic_Sample", "Simple_JackLocomotion",
- "SampleFullscreenAnimation", "UiDrawCallsSample", "UiFeatures", "UiIn3DWorld", "UiMainMenuLuaSample",
- "UiMainMenuScriptCanvasSample", "UiTextureAtlasSample"],
- marks=pytest.mark.skipif(not WINDOWS_LAUNCHER, reason="Only supported on Windows hosts")),
- pytest.param("win_x64_vs2019", "debug", "SamplesProject", "all", "Advanced_RinLocomotion",
- ["Advanced_RinLocomotion", "Audio_Sample", "Fur_Technical_Sample", "Gems_InAppPurchases_Sample",
- "Metastream_Sample", "ScriptCanvas_Basic_Sample", "Simple_JackLocomotion",
- "SampleFullscreenAnimation", "UiDrawCallsSample", "UiFeatures", "UiIn3DWorld", "UiMainMenuLuaSample",
- "UiMainMenuScriptCanvasSample", "UiTextureAtlasSample"],
- marks=pytest.mark.skipif(not WINDOWS_LAUNCHER, reason="Only supported on Windows hosts"))
- ])
-class TestMultipleLevels(object):
- def test_multiple_levels(self, launcher_instance, levels, remote_console_instance):
- """
- Verifies projects with multiple demo-levels can compile and successfully launch.
- """
- configure_setup(launcher_instance)
-
- assert_build_success(launcher_instance)
- assert_process_assets(launcher_instance)
-
- start_launcher(launcher_instance)
- start_remote_console(launcher_instance, remote_console_instance)
-
- for level in levels:
- load_level(launcher_instance, remote_console_instance, level)
- remote_console_take_screenshot(launcher_instance, remote_console_instance, level)
diff --git a/Tests/graphics/__init__.py b/Tests/graphics/__init__.py
deleted file mode 100755
index 6ed3dc4bda..0000000000
--- a/Tests/graphics/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""
-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/Tests/graphics/ly107748_LightningArcProperties.cfg b/Tests/graphics/ly107748_LightningArcProperties.cfg
deleted file mode 100644
index fe05f5f782..0000000000
--- a/Tests/graphics/ly107748_LightningArcProperties.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-# this file is copied to $/dev/editor_autoexec.cfg so the the Editor automation runs for this Hydra test
-pyRunFile @devroot@/Tests/graphics/ly107748_LightningArcProperties_test_case.py
\ No newline at end of file
diff --git a/Tests/graphics/ly107748_LightningArcProperties_test.py b/Tests/graphics/ly107748_LightningArcProperties_test.py
deleted file mode 100755
index 59cc3d8d1b..0000000000
--- a/Tests/graphics/ly107748_LightningArcProperties_test.py
+++ /dev/null
@@ -1,83 +0,0 @@
-"""
-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 is a pytest module to test the in-Editor Python API from PythonEditorFuncs
-#
-import pytest
-pytest.importorskip('test_tools')
-import time
-import logging
-import os
-import shutil
-
-from test_tools import WINDOWS_LAUNCHER
-import test_tools.shared.log_monitor
-import test_tools.launchers.phase
-import test_tools.builtin.fixtures as fixtures
-
-# Use the built-in workspace and editor fixtures.
-# These will configure the requested project and run the editor.
-workspace = fixtures.use_fixture(fixtures.builtin_workspace_fixture, scope='function')
-editor = fixtures.use_fixture(fixtures.editor, scope='function')
-
-logger = logging.getLogger(__name__)
-
-
-@pytest.mark.parametrize("platform,configuration,project,spec", [
- pytest.param("win_x64_vs2017", "profile", "AutomatedTesting", "all", marks=pytest.mark.skipif(not WINDOWS_LAUNCHER, reason="Only supported on Windows hosts")),
-])
-class TestLightningArcPropertyRanges(object):
-
- @pytest.fixture(autouse=True)
- def setup_teardown(self, request, workspace, editor):
- def teardown():
- editor.ensure_stopped()
-
- file_utils.delete_level(editor, "LightningArcTestLevel")
-
- request.addfinalizer(teardown)
-
- def test_change_properties(self, request, editor, project):
- logger.debug("Running automated test")
-
- request.addfinalizer(editor.ensure_stopped)
-
- editor.deploy()
- editor.launch(["--exec", "@engroot@/Tests/graphics/ly107748_LightningArcProperties.cfg"])
-
- editorlog_file = os.path.join(editor.workspace.release.paths.project_log(), 'Editor.log')
-
- # LY-107861 LY-108088
- # expected failure cases are commented out pending implementation of property validation by hydra
- expected_lines = [
- "Created new entity.",
- "Lightning Arc component added to entity.",
- #"ChangeProperty m_config|Arc Parameters|Segment Count to 0 failed.",
- "ChangeProperty m_config|Arc Parameters|Segment Count to 1 succeeded.",
- "ChangeProperty m_config|Arc Parameters|Segment Count to 25 succeeded.",
- "ChangeProperty m_config|Arc Parameters|Segment Count to 50 succeeded.",
- "ChangeProperty m_config|Arc Parameters|Segment Count to 70 succeeded.",
- #"ChangeProperty m_config|Arc Parameters|Segment Count to 75 failed.",
- #"ChangeProperty m_config|Arc Parameters|Segment Count to 100 failed.",
- #"ChangeProperty m_config|Arc Parameters|Point Count to 0 failed.",
- "ChangeProperty m_config|Arc Parameters|Point Count to 1 succeeded.",
- "ChangeProperty m_config|Arc Parameters|Point Count to 25 succeeded.",
- "ChangeProperty m_config|Arc Parameters|Point Count to 50 succeeded.",
- "ChangeProperty m_config|Arc Parameters|Point Count to 70 succeeded.",
- #"ChangeProperty m_config|Arc Parameters|Point Count to 75 failed.",
- #"ChangeProperty m_config|Arc Parameters|Point Count to 100 failed.",
- ]
-
- test_tools.shared.log_monitor.monitor_for_expected_lines(editor, editorlog_file, expected_lines)
-
- # Rely on the test script to quit after running
- editor.run(test_tools.launchers.phase.WaitForLauncherToQuit(editor, 10))
diff --git a/Tests/graphics/ly107748_LightningArcProperties_test_case.py b/Tests/graphics/ly107748_LightningArcProperties_test_case.py
deleted file mode 100755
index dd0c72e893..0000000000
--- a/Tests/graphics/ly107748_LightningArcProperties_test_case.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""
-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.
-"""
-
-# Tests a portion of the Component Property Get/Set Python API while the Editor is running
-
-import azlmbr.legacy.general as general
-import azlmbr.bus as bus
-import azlmbr.entity as entity
-import azlmbr.editor as editor
-import azlmbr.math as math
-
-
-# Create a test level
-general.create_level_no_prompt("LightningArcTestLevel", 1024, 1, 1024, True)
-
-def ChangeProperty(component, path, value):
- getPropertyOutcome = editor.EditorComponentAPIBus(bus.Broadcast, 'GetComponentProperty', component, path)
- if not(getPropertyOutcome.IsSuccess()):
- print("GetComponentProperty " + path + " failed.")
- else:
- oldValue = getPropertyOutcome.GetValue()
-
- setPropertyOutcome = editor.EditorComponentAPIBus(bus.Broadcast, 'SetComponentProperty', component, path, value)
- if not(setPropertyOutcome.IsSuccess()):
- print("SetComponentProperty " + path + " to " + str(value) + " failed.")
-
- getPropertyOutcome = editor.EditorComponentAPIBus(bus.Broadcast, 'GetComponentProperty', component, path)
- if not(getPropertyOutcome.IsSuccess()):
- print("GetComponentProperty " + path + " failed.")
- else:
- newValue = getPropertyOutcome.GetValue()
-
- if not(newValue == oldValue):
- print("ChangeProperty " + path + " to " + str(value) + " succeeded.")
- else:
- print("ChangeProperty " + path + " to " + str(value) + " failed.")
-
-# Create new Entity
-entityId = editor.ToolsApplicationRequestBus(bus.Broadcast, 'CreateNewEntity', entity.EntityId())
-
-if (entityId.IsValid()):
- print("Created new entity.")
-
-# Get Component Type for Lightning Arc
-typeIdsList = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeIdsByEntityType', ["Lightning Arc"], entity.EntityType().Game)
-
-componentOutcome = editor.EditorComponentAPIBus(bus.Broadcast, 'AddComponentsOfType', entityId, typeIdsList)
-
-if (componentOutcome.IsSuccess()):
- print("Lightning Arc component added to entity.")
-
-components = componentOutcome.GetValue()
-component = components[0]
-
-# Tests for GetComponentProperty/SetComponentProperty
-ChangeProperty(component, "m_config|Arc Parameters|Segment Count", 0)
-ChangeProperty(component, "m_config|Arc Parameters|Segment Count", 1)
-ChangeProperty(component, "m_config|Arc Parameters|Segment Count", 25)
-ChangeProperty(component, "m_config|Arc Parameters|Segment Count", 50)
-ChangeProperty(component, "m_config|Arc Parameters|Segment Count", 70)
-ChangeProperty(component, "m_config|Arc Parameters|Segment Count", 75)
-ChangeProperty(component, "m_config|Arc Parameters|Segment Count", 100)
-
-ChangeProperty(component, "m_config|Arc Parameters|Point Count", 0)
-ChangeProperty(component, "m_config|Arc Parameters|Point Count", 1)
-ChangeProperty(component, "m_config|Arc Parameters|Point Count", 25)
-ChangeProperty(component, "m_config|Arc Parameters|Point Count", 50)
-ChangeProperty(component, "m_config|Arc Parameters|Point Count", 70)
-ChangeProperty(component, "m_config|Arc Parameters|Point Count", 75)
-ChangeProperty(component, "m_config|Arc Parameters|Point Count", 100)
-
-general.exit_no_prompt()
diff --git a/Tests/hydra/ctests/open_level_tweak_and_exit.py b/Tests/hydra/ctests/open_level_tweak_and_exit.py
deleted file mode 100755
index de81327852..0000000000
--- a/Tests/hydra/ctests/open_level_tweak_and_exit.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""
-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.
-"""
-
-# --runpythontest @devroot@\tests\hydra\ctests\open_level_tweak_and_exit.py
-# An example of how a create a level, make an entity, and terminate successfully
-
-import time
-import azlmbr.editor
-import azlmbr.entity
-import azlmbr.framework
-import azlmbr.legacy.general as general
-from azlmbr.bus import Broadcast
-
-handler = None
-
-def open_level(level):
- print('opening level {}'.format(level))
- azlmbr.editor.EditorToolsApplicationRequestBus(Broadcast, 'OpenLevelNoPrompt', level)
- general.idle_wait(1.0)
-
-def on_entity_registered(args):
- print('on_entity_registered')
- azlmbr.framework.Terminate(0)
-
-def main():
- print ('open_level_tweak_and_exit - starting')
- open_level('auto_test')
-
- azlmbr.editor.ToolsApplicationRequestBus(Broadcast, 'CreateNewEntity', azlmbr.entity.EntityId())
- general.idle_wait(1.0)
-
- handler = azlmbr.editor.ToolsApplicationNotificationBusHandler()
- handler.connect()
- handler.add_callback('EntityRegistered', on_entity_registered)
- azlmbr.editor.ToolsApplicationRequestBus(Broadcast, 'CreateNewEntity', azlmbr.entity.EntityId())
-
-if __name__ == "__main__":
- main()
diff --git a/Tests/hydra/ctests/start_stop.py b/Tests/hydra/ctests/start_stop.py
deleted file mode 100755
index c8fd670a97..0000000000
--- a/Tests/hydra/ctests/start_stop.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""
-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.
-"""
-
-# --runpythontest @devroot@\tests\hydra\ctests\start_stop.py
-# an example of a test script that loads a level, listens for the first entity, and terminates the Editor with a 0
-
-import azlmbr.framework
-import azlmbr.editor
-import azlmbr.bus
-
-handler = None
-
-def on_entity_registered(args):
- print('on_entity_registered')
- azlmbr.framework.Terminate(0)
-
-def main():
- print ('hello, start_stop')
- handler = azlmbr.editor.ToolsApplicationNotificationBusHandler()
- handler.connect()
- handler.add_callback('EntityRegistered', on_entity_registered)
- azlmbr.editor.EditorToolsApplicationRequestBus(azlmbr.bus.Broadcast, 'OpenLevelNoPrompt', 'auto_test')
- print ('start_stop started')
-
-if __name__ == "__main__":
- main()
diff --git a/Tests/hydra/ctests/start_with_args.py b/Tests/hydra/ctests/start_with_args.py
deleted file mode 100755
index e8b99b54ac..0000000000
--- a/Tests/hydra/ctests/start_with_args.py
+++ /dev/null
@@ -1,28 +0,0 @@
-"""
-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.
-"""
-
-# --runpythontest @devroot@\tests\hydra\ctests\start_with_args.py --runpythonargs foo bar baz
-# An example of how to use runpythontest with a main() + args
-
-import azlmbr.framework
-
-def main():
- print("hello, start_with_args")
-
- # print command line arguments
- for arg in sys.argv:
- print (arg)
-
- azlmbr.framework.Terminate(0)
-
-if __name__ == "__main__":
- main()
-
diff --git a/Tests/hydra/ctests/stop_with_error_one.py b/Tests/hydra/ctests/stop_with_error_one.py
deleted file mode 100755
index b17b5247aa..0000000000
--- a/Tests/hydra/ctests/stop_with_error_one.py
+++ /dev/null
@@ -1,17 +0,0 @@
-"""
-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.
-"""
-
-# --runpythontest @devroot@\tests\hydra\ctests\stop_with_error_one.py
-# an example terminating with a non-zero return code from Editor.exe
-
-import azlmbr.framework
-print ('hello, stop_with_error_one')
-azlmbr.framework.Terminate(1)
diff --git a/Tests/hydra/ctests/stop_with_zero.py b/Tests/hydra/ctests/stop_with_zero.py
deleted file mode 100755
index d1d3f4f58d..0000000000
--- a/Tests/hydra/ctests/stop_with_zero.py
+++ /dev/null
@@ -1,17 +0,0 @@
-"""
-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.
-"""
-
-# --runpythontest @devroot@\tests\hydra\ctests\stop_with_zero.py
-# An example of how a test script stops the Editor.exe with a succuessful zero return code
-
-import azlmbr.framework
-print ('hello, stop_with_zero')
-azlmbr.framework.Terminate(0)
diff --git a/Tests/hydra/ctests/throws_exception.py b/Tests/hydra/ctests/throws_exception.py
deleted file mode 100755
index e8b4507fb1..0000000000
--- a/Tests/hydra/ctests/throws_exception.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""
-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.
-"""
-
-# --runpythontest @devroot@\tests\hydra\ctests\throws_exception.py
-# An example of how a test script to fatal from Editor.exe when a Python exception happens
-
-print ('hello, throws_exception')
-foo = 1.0
-bar = 0.0
-baz = foo / bar
-
diff --git a/Tests/ly_shared/PlatformSetting.py b/Tests/ly_shared/PlatformSetting.py
deleted file mode 100755
index b215275c16..0000000000
--- a/Tests/ly_shared/PlatformSetting.py
+++ /dev/null
@@ -1,69 +0,0 @@
-"""
-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.
-
-Class for querying and setting a system setting/preference.
-
-"""
-
-import pytest
-import logging
-from typing import Optional, Any
-
-import ly_test_tools.o3de.pipeline_utils as utils
-
-logger = logging.getLogger(__name__)
-
-
-class PlatformSetting:
- """
- Interface for managing different platforms' system variables.
- """
-
- class DATA_TYPE:
- """Platform-agnostic data type enums"""
-
- INT = 1
- STR = 2
- STR_LIST = 3
-
- def __init__(self, workspace: pytest.fixture, subkey: str, key: str) -> None:
- self._workspace = workspace
- self._key = key
- self._subkey = subkey
-
- def get_value(self, get_type: bool = False) -> object:
- """Gets the current setting's value (and optionally type as tuple) from the system. Returns None if entry DNE"""
- raise NotImplementedError("Virtual PlatformSetting not implemented. Instantiate a specific platform")
-
- def set_value(self, value: any) -> bool:
- """Sets the current setting's value. Creates the entry if it DNE. Returns True for success."""
- raise NotImplementedError("Virtual PlatformSetting not implemented. Instantiate a specific platform")
-
- def delete_entry(self) -> bool:
- """Deletes the settings entry. Returns boolean for success."""
- raise NotImplementedError("Virtual PlatformSetting not implemented. Instantiate a specific platform")
-
- def entry_exists(self) -> bool:
- """Checks if the settings entry exists."""
- raise NotImplementedError("Virtual PlatformSetting not implemented. Instantiate a specific platform")
-
- @staticmethod
- def get_system_setting(workspace: pytest.fixture, subkey: str, key: str, hive: Optional[str] = None) -> Any:
- """Factory method creates a platform-specific system setting accessor"""
- if workspace.asset_processor_platform is 'windows':
- # import WindowsSetting and return an instance
- from Tests.ly_shared.WindowsRegistrySetting import WindowsRegistrySetting
-
- return WindowsRegistrySetting(workspace, subkey, key, hive)
- # ########################################################
- # Insert Mac (and Linux?) Setting implementations
- # ########################################################
- else:
- raise NotImplementedError(f"Platform: {workspace.platform} not supported yet")
diff --git a/Tests/ly_shared/PlatformSettingTest.py b/Tests/ly_shared/PlatformSettingTest.py
deleted file mode 100755
index f0ef8b6d1a..0000000000
--- a/Tests/ly_shared/PlatformSettingTest.py
+++ /dev/null
@@ -1,92 +0,0 @@
-"""
-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.
-
-Tests the functionality of the PlatformSetting class
-"""
-
-import pytest
-
-import ly_test_tools.builtin.helpers as helpers
-from Tests.ly_shared.PlatformSetting import PlatformSetting
-
-all_platforms = helpers.all_host_platforms_params()
-automatic_platform_skipping = helpers.automatic_platform_skipping
-targetProjects = ["Helios"]
-
-
-@pytest.mark.usefixtures("automatic_platform_skipping")
-@pytest.mark.parametrize("platform", all_platforms)
-@pytest.mark.parametrize("configuration", ["profile"])
-@pytest.mark.parametrize("spec", ["all"])
-@pytest.mark.parametrize("project", targetProjects)
-class TestsPlatformSetting(object):
- """
- Tests for the PlatformSetting class
- """
-
- def test_PlatformSetting(self, workspace):
-
- key = "Software"
- subkey = "TemporarySystemSetting"
-
- # Create setting reference
- setting = PlatformSetting.get_system_setting(workspace, subkey, key)
-
- # Test storing integer
- value = 74
- setting.set_value(value)
-
- # Test creation of subkey
- assert setting.entry_exists(), f"Failed creating key:subkey, {key}:{subkey}"
-
- # Test data retrieval (without type)
- retrieved = setting.get_value()
- # fmt:off
- assert retrieved == value, \
- f"Unexpected value retrieved from system settings. Expected: {value}, Actual: {retrieved}"
- # fmt:on
-
- # Test data retrieval (with type)
- retrieved = setting.get_value(get_type=True)
- assert type(retrieved) == tuple, "Getting value with type DID NOT return a tuple"
- assert len(retrieved) == 2, f"Getting value with type returned a tuple of size {len(retrieved)}: expected 2"
- assert retrieved[1] == PlatformSetting.DATA_TYPE.INT, "Value stored was int, but type retrieved was NOT int"
- assert type(retrieved[0]) == int, "Value stored was int, but value retrieved was NOT int"
-
- # fmt:off
- assert retrieved[0] == value, \
- f"Unexpected value retrieved from system settings. Expected: {value}, Actual: {retrieved[0]}"
- # fmt:on
-
- # Test storing string
- value = "Some Text"
- setting.set_value(value)
- retrieved = setting.get_value(get_type=True)
- assert (
- retrieved[1] == PlatformSetting.DATA_TYPE.STR
- ), "Value stored was string, but type retrieved was NOT string"
- assert type(retrieved[0]) == str, "Value stored was string, but value retrieved was NOT string"
- assert value == retrieved[0], f"Value retrieved not expected. Expected: {value}, Actual: {retrieved[0]}"
-
- # Test storing list of strings
- value = ["Some", "List", "Of", "Text"]
- setting.set_value(value)
- retrieved = setting.get_value(get_type=True)
- assert (
- retrieved[1] == PlatformSetting.DATA_TYPE.STR_LIST
- ), "Value stored was string list, but type retrieved was NOT string list"
- assert type(retrieved[0]) == list, "Value stored was string, but value retrieved was NOT string"
- # fmt:off
- assert sorted(value) == sorted(retrieved[0]), f"Value retrieved not expected. " \
- f"Expected: {value}, Actual: {retrieved[0]}"
- # fmt:on
-
- setting.delete_entry()
- assert not setting.entry_exists(), f"Failed to delete key:subkey, {key}:{subkey}"
diff --git a/Tests/ly_shared/WindowsRegistrySetting.py b/Tests/ly_shared/WindowsRegistrySetting.py
deleted file mode 100755
index c1aa4e4a98..0000000000
--- a/Tests/ly_shared/WindowsRegistrySetting.py
+++ /dev/null
@@ -1,165 +0,0 @@
-"""
-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.
-
-Class for querying and setting a windows registry setting.
-
-"""
-import pytest
-import logging
-from typing import List, Optional, Tuple, Any
-
-from winreg import (
- CreateKey,
- OpenKey,
- QueryValueEx,
- DeleteValue,
- SetValueEx,
- KEY_ALL_ACCESS,
- KEY_WRITE,
- REG_SZ,
- REG_MULTI_SZ,
- REG_DWORD,
- HKEY_CURRENT_USER,
-)
-
-
-from Tests.ly_shared.PlatformSetting import PlatformSetting
-
-logger = logging.getLogger(__name__)
-
-
-class WindowsRegistrySetting(PlatformSetting):
- def __init__(self, workspace: pytest.fixture, subkey: str, key: str, hive: Optional[str] = None) -> None:
- super().__init__(workspace, subkey, key)
- self._hive = None
- try:
- if hive is not None:
- self._hive = self._str_to_hive(hive)
- except ValueError:
- logger.warning(f"Windows Registry Hive {hive} not recognized, using default: HKEY_CURRENT_USER")
- finally:
- if self._hive is None:
- self._hive = HKEY_CURRENT_USER
-
- def get_value(self, get_type: Optional[bool] = False) -> Any:
- """Retrieves the fast scan value in Windows registry (and optionally the type). If entry DNE, returns None."""
- if self.entry_exists():
- registryKey = OpenKey(self._hive, self._key)
- value = QueryValueEx(registryKey, self._subkey)
- registryKey.Close()
- # Convert windows data type to universal data type flag: PlatformSettings.DATA_TYPE
- # And handles unicode conversion for strings
- value = self._convert_value(value)
- return value if get_type else value[0]
-
- else:
- logger.warning(f"Could not retrieve Registry entry; key: {self._key}, subkey: {self._subkey}.")
- return None
-
- def set_value(self, value: Any) -> bool:
- """Sets the Windows registry value."""
- value, win_type = self._format_data(value)
- registryKey = None
- result = False
- try:
- CreateKey(self._hive, self._subkey)
- registryKey = OpenKey(self._hive, self._key, 0, KEY_WRITE)
- SetValueEx(registryKey, self._subkey, 0, win_type, value)
- result = True
- except WindowsError as e:
- logger.warning(f"Windows error caught while setting fast scan registry: {e}")
- finally:
- if registryKey is not None:
- # Close key if it's been opened successfully
- registryKey.Close()
- return result
-
- def delete_entry(self) -> bool:
- """Deletes the Windows registry entry for fast scan enabled"""
- try:
- if self.entry_exists():
- registryKey = OpenKey(self._hive, self._key, 0, KEY_ALL_ACCESS)
- DeleteValue(registryKey, self._subkey)
- registryKey.Close()
- return True
- except WindowsError:
- logger.error(f"Could not delete registry entry; key: {self._key}, subkey: {self._subkey}")
- finally:
- return False
-
- def entry_exists(self) -> bool:
- """Checks for existence of the setting in Windows registry."""
- try:
- # Attempt to open and query key. If fails then the entry DNE
- registryKey = OpenKey(self._hive, self._key)
- QueryValueEx(registryKey, self._subkey)
- registryKey.Close()
- return True
-
- except WindowsError:
- return False
-
- @staticmethod
- def _format_data(value: bool or int or str or List[str]) -> Tuple[int or str or List[str], int]:
- """Formats the type of the value provided. Returns the formatted value and the windows registry type (int)."""
- if type(value) == str:
- return value, REG_SZ
- elif type(value) == bool:
- value = "true" if value else "false"
- return value, REG_SZ
- elif type(value) == int or type(value) == float:
- if type(value) == float:
- logger.warning(f"Windows registry does not support floats. Truncating {value} to integer")
- value = int(value)
- return value, REG_DWORD
- elif type(value) == list:
- for single_value in value:
- if type(single_value) != str:
- # fmt:off
- raise ValueError(
- f"Windows Registry lists only support strings, got a {type(single_value)} in the list")
- # fmt:on
- return value, REG_MULTI_SZ
- else:
- raise ValueError(f"Windows registry expected types: int, str and [str], found {type(value)}")
-
- @staticmethod
- def _convert_value(value_tuple: Tuple[Any, int]) -> Tuple[Any, PlatformSetting.DATA_TYPE]:
- """Converts the Windows registry data and type (tuple) to a (standardized) data and PlatformSetting.DATA_TYPE"""
- value, windows_type = value_tuple
- if windows_type == REG_SZ:
- # Convert from unicode to string
- return value, PlatformSetting.DATA_TYPE.STR
- elif windows_type == REG_MULTI_SZ:
- # Convert from unicode to string
- return [string for string in value], PlatformSetting.DATA_TYPE.STR_LIST
- elif windows_type == REG_DWORD:
- return value, PlatformSetting.DATA_TYPE.INT
- else:
- raise ValueError(f"Type flag not recognized: {windows_type}")
-
- @staticmethod
- def _str_to_hive(hive_str: str) -> int:
- """Converts a string to a Windows Registry Hive enum (int)"""
- from winreg import HKEY_CLASSES_ROOT, HKEY_CURRENT_CONFIG, HKEY_LOCAL_MACHINE, HKEY_USERS
-
- lower = hive_str.lower()
- if lower == "hkey_current_user" or lower == "current_user":
- return HKEY_CURRENT_USER
- elif lower == "hkey_classes_root" or lower == "classes_root":
- return HKEY_CLASSES_ROOT
- elif lower == "hkey_current_config" or lower == "current_config":
- return HKEY_CURRENT_CONFIG
- elif lower == "hkey_local_machine" or lower == "local_machine":
- return HKEY_LOCAL_MACHINE
- elif lower == "hkey_users" or lower == "users":
- return HKEY_USERS
- else:
- raise ValueError(f"Hive: {hive_str} not recognized")
diff --git a/Tests/ly_shared/__init__.py b/Tests/ly_shared/__init__.py
deleted file mode 100755
index 6ed3dc4bda..0000000000
--- a/Tests/ly_shared/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""
-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/Tests/ly_shared/asset_database_utils.py b/Tests/ly_shared/asset_database_utils.py
deleted file mode 100755
index da8ad3651e..0000000000
--- a/Tests/ly_shared/asset_database_utils.py
+++ /dev/null
@@ -1,83 +0,0 @@
-"""
-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 sqlite3
-import os
-from typing import List
-
-# Index for ProductID in Products table in DB
-PRODUCT_ID_INDEX = 0
-
-
-def do_select(asset_db_path, cmd):
- try:
- connection = sqlite3.connect(asset_db_path)
- # Get ProductID from database
- db_rows = connection.execute(cmd)
- return_result = db_rows.fetchall()
- connection.close()
- return return_result
- except sqlite3.Error as sqlite_error:
- print(f'select on db {asset_db_path} failed with exception {sqlite_error}')
- return []
-
-
-def get_active_platforms_from_db(asset_db_path) -> List[str]:
- """Returns a list of platforms that are active in the database, based on what jobs were run"""
- platform_rows = do_select(asset_db_path, f"select distinct Platform from Jobs")
- # Condense this into a single list of platforms.
- platforms = [platform[0] for platform in platform_rows]
- return platforms
-
-
-# Convert a source product path into a db product path
-# cache_platform/projectname/product_path
-def get_db_product_path(workspace, source_path, cache_platform):
- product_path = os.path.join(cache_platform, workspace.project, source_path)
- product_path = product_path.replace('\\', '/')
- return product_path
-
-
-def get_product_id(asset_db_path, product_name) -> str:
- # Get ProductID from database
- product_id = list(do_select(asset_db_path, f"SELECT ProductID FROM Products where ProductName='{product_name}'"))
- if len(product_id) == 0:
- return product_id # return empty list
- return product_id[0][PRODUCT_ID_INDEX] # Get product id from 'first' row
-
-
-# Retrieve a product_id given a source_path assuming the source is copied into the cache with the same
-# name or a product name without cache_platform or projectname prepended
-def get_product_id_from_relative(workspace, source_path, asset_platform):
- return get_product_id(workspace.paths.asset_db(), get_db_product_path(workspace, source_path, asset_platform))
-
-
-def get_missing_dependencies(asset_db_path, product_id) -> List[str]:
- return list(do_select(asset_db_path, f"SELECT * FROM MissingProductDependencies where ProductPK={product_id}"))
-
-
-def do_single_transaction(asset_db_path, cmd):
- try:
- connection = sqlite3.connect(asset_db_path)
- cursor = connection.cursor() # SQL cursor used for issuing commands
- cursor.execute(cmd)
- connection.commit() # Save changes
- connection.close()
- except sqlite3.Error as sqlite_error:
- print(f'transaction on db {asset_db_path} cmd {cmd} failed with exception {sqlite_error}')
-
-
-def clear_missing_dependencies(asset_db_path, product_id) -> None:
- do_single_transaction(asset_db_path, f"DELETE FROM MissingProductDependencies where ProductPK={product_id}")
-
-
-def clear_all_missing_dependencies(asset_db_path) -> None:
- do_single_transaction(asset_db_path, "DELETE FROM MissingProductDependencies;")
diff --git a/Tests/ly_shared/asset_processor_utils.py b/Tests/ly_shared/asset_processor_utils.py
deleted file mode 100755
index 13dc50ea83..0000000000
--- a/Tests/ly_shared/asset_processor_utils.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""
-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 logging
-import os
-import subprocess
-
-from ly_test_tools.environment.process_utils import kill_processes_named as kill_processes_named
-logger = logging.getLogger(__name__)
-
-
-def start_asset_processor(bin_dir):
- """
- Starts the AssetProcessor from the given bin directory. Raises a RuntimeError if the process fails.
- :param bin_dir: The bin directory from which to launch the AssetProcessor executable.
- :return: A subprocess.Popen object for the AssetProcessor process.
- """
- os.chdir(bin_dir)
- asset_processor = subprocess.Popen(['AssetProcessor.exe'])
- return_code = asset_processor.poll()
-
- if return_code is not None and return_code != 0:
- logger.error("Failed to start AssetProcessor")
- raise RuntimeError("AssetProcessor exited with code {}".format(return_code))
- else:
- logger.info("AssetProcessor is running")
- return asset_processor
-
-
-def kill_asset_processor():
- """
- Kill the AssetProcessor and all its related processes .
- """
-
- kill_processes_named('AssetProcessor_tmp', ignore_extensions=True)
- kill_processes_named('AssetProcessor', ignore_extensions=True)
- kill_processes_named('AssetProcessorBatch', ignore_extensions=True)
- kill_processes_named('AssetBuilder', ignore_extensions=True)
- kill_processes_named('rc', ignore_extensions=True)
-
-
-
diff --git a/Tests/ly_shared/file_utils.py b/Tests/ly_shared/file_utils.py
deleted file mode 100755
index 2fc93d7538..0000000000
--- a/Tests/ly_shared/file_utils.py
+++ /dev/null
@@ -1,169 +0,0 @@
-"""
-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 shutil
-import logging
-import stat
-
-import ly_test_tools.environment.file_system as file_system
-import ly_test_tools.environment.waiter as waiter
-
-logger = logging.getLogger(__name__)
-
-
-def clear_out_file(file_path):
- """
- Clears out the specified config file to be empty.
- :param file_path: The full path to the file.
- """
- if os.path.exists(file_path):
- file_system.unlock_file(file_path)
- with open(file_path, 'w') as file_to_write:
- file_to_write.write('')
- else:
- logger.debug(f'{file_path} not found while attempting to clear out file.')
-
-
-def add_commands_to_config_file(config_file_dir, config_file_name, command_list):
- """
- From the command list, appends each command to the specified config file.
- :param config_file_dir: The directory the config file is contained in.
- :param config_file_name: The config file name.
- :param command_list: The commands to add to the file.
- :return:
- """
- config_file_path = os.path.join(config_file_dir, config_file_name)
- os.chmod(config_file_path, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
- with open(config_file_path, 'w') as launch_config_file:
- for command in command_list:
- launch_config_file.write("{}\n".format(command))
-
-
-def gather_error_logs(workspace):
- """
- Grabs all error logs (if there are any) and puts them into the specified logs path.
- :param workspace: The AbstractWorkspaceManager object that contains all of the paths
- """
- error_log_path = os.path.join(workspace.paths.project_log(), 'error.log')
- error_dump_path = os.path.join(workspace.paths.project_log(), 'error.dmp')
- if os.path.exists(error_dump_path):
- workspace.artifact_manager.save_artifact(error_dump_path)
- if os.path.exists(error_log_path):
- workspace.artifact_manager.save_artifact(error_log_path)
-
-
-def delete_screenshot_folder(workspace):
- """
- Deletes screenshot folder from platform path
- :param workspace: The AbstractWorkspaceManager object that contains all of the paths
- """
- shutil.rmtree(workspace.paths.project_screenshots(), ignore_errors=True)
-
-
-def move_file(src_dir, dest_dir, file_name, timeout=120):
- """
- Attempts to move a file from the source directory to the destination directory. Raises an IOError if
- the file is in use.
- :param src_dir: Directory of the file to be moved.
- :param dest_dir: Directory where the file will be moved to.
- :param file_name: Name of the file to be moved.
- :param timeout: Number of seconds to wait for the file to be released.
- """
- file_path = os.path.join(src_dir, file_name)
- if os.path.exists(file_path):
- waiter.wait_for(lambda: move_file_check(src_dir, dest_dir, file_name), timeout=timeout,
- exc=IOError('Cannot move file {} while in use'.format(file_path)))
-
-
-def move_file_check(src_dir, dest_dir, file_name):
- """
- Moves file and checks if the file has been moved from the source to the destination directory.
- :param src_dir: Source directory of the file to be moved
- :param dest_dir: Destination directory where the file should move to
- :param file_name: The name of the file to be moved
- :return:
- """
- try:
- shutil.move(os.path.join(src_dir, file_name), os.path.join(dest_dir, file_name))
- except OSError as e:
- logger.info(e)
- return False
-
- return True
-
-
-def rename_file(file_path, dest_path, timeout=10):
- # type: (str, str, int) -> None
- """
- Renames a file by moving it. Waits for file to become available and raises and exception if timeout occurs.
- :param file_path: absolute path to the source file
- :param dest_path: absolute path to the new file
- :param timeout: timeout to wait for function to complete
- :return: None
- """
- def _rename_file_check():
- try:
- shutil.move(file_path, dest_path)
- except OSError as e:
- logger.debug(f'Attempted to rename file: {file_path} but an error occurred, retrying.'
- f'\nError: {e}',
- stackinfo=True)
- return False
- return True
-
- if os.path.exists(file_path):
- waiter.wait_for(lambda: _rename_file_check(), timeout=timeout,
- exc=OSError('Cannot rename file {} while in use'.format(file_path)))
-
-
-def delete_level(workspace, level_dir, timeout=120):
- """
- Attempts to delete an entire level folder from the project.
- :param workspace: The workspace instance to delete the level from.
- :param level_dir: The level folder to delete
- """
-
- if not level_dir:
- logger.warning("level_dir is empty, nothing to delete.")
- return
-
- full_level_dir = os.path.join(workspace.paths.project(), 'Levels', level_dir)
- if not os.path.isdir(full_level_dir):
- if os.path.exists(full_level_dir):
- logger.error("level '{}' isn't a directory, it won't be deleted.".format(full_level_dir))
- else:
- logger.info("level '{}' doesn't exist, nothing to delete.".format(full_level_dir))
- return
-
- waiter.wait_for(lambda: delete_check(full_level_dir),
- timeout=timeout,
- exc=IOError('Cannot delete directory {} while in use'.format(full_level_dir)))
-
-def delete_check(src_dir):
- """
- Deletes directory and verifies that it's been deleted.
- :param src_dir: The directory to delete
- """
- try:
- def handle_delete_error(action, path, exception_info):
- logger.info("Error deleting '{}' ({}), changing permissions to writeable.".format(path, exception_info))
- os.chmod(path, stat.S_IWRITE)
- # Try the passed-in action (delete) again
- action(path)
-
- shutil.rmtree(src_dir, onerror=handle_delete_error)
- except OSError as e:
- logger.debug("Delete for '{}' failed: {}".format(src_dir, e))
- return False
-
- return not os.path.exists(src_dir)
-
diff --git a/Tests/ly_shared/hydra_editor_utils.py b/Tests/ly_shared/hydra_editor_utils.py
deleted file mode 100755
index b75e680cf8..0000000000
--- a/Tests/ly_shared/hydra_editor_utils.py
+++ /dev/null
@@ -1,340 +0,0 @@
-"""
-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 as bus
-import azlmbr.editor as editor
-import azlmbr.entity as entity
-import azlmbr.object
-
-from typing import List
-from math import isclose
-import collections.abc
-
-
-def find_entity_by_name(entity_name):
- """
- Gets an entity ID from the entity with the given entity_name
- :param entity_name: String of entity name to search for
- :return entity ID
- """
- search_filter = entity.SearchFilter()
- search_filter.names = [entity_name]
- matching_entity_list = entity.SearchBus(bus.Broadcast, 'SearchEntities', search_filter)
- if matching_entity_list:
- matching_entity = matching_entity_list[0]
- if matching_entity.IsValid():
- print(f'{entity_name} entity found with ID {matching_entity.ToString()}')
- return matching_entity
- else:
- return matching_entity_list
-
-
-def get_component_type_id(component_name):
- """
- Gets the component_type_id from a given component name
- :param component_name: String of component name to search for
- :return component type ID
- """
- type_ids_list = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeIdsByEntityType', [component_name], entity.EntityType().Game)
- component_type_id = type_ids_list[0]
- return component_type_id
-
-
-def add_component(componentName, entityId):
- """
- Given a component name, finds component TypeId, adds to given entity, and verifies successful add/active state.
- :param componentName: String of component name to add.
- :param entityId: Entity to add component to.
- :return: Component object.
- """
- typeIdsList = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeIdsByEntityType', [componentName], entity.EntityType().Game)
- typeNamesList = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeNames', typeIdsList)
- componentOutcome = editor.EditorComponentAPIBus(bus.Broadcast, 'AddComponentsOfType', entityId, typeIdsList)
- isActive = editor.EditorComponentAPIBus(bus.Broadcast, 'IsComponentEnabled', componentOutcome.GetValue()[0])
- hasComponent = editor.EditorComponentAPIBus(bus.Broadcast, 'HasComponentOfType', entityId, typeIdsList[0])
- if componentOutcome.IsSuccess() and isActive:
- print('{} component was added to entity'.format(typeNamesList[0]))
- elif componentOutcome.IsSuccess() and not isActive:
- print('{} component was added to entity, but the component is disabled'.format(typeNamesList[0]))
- elif not componentOutcome.IsSuccess():
- print('Failed to add {} component to entity'.format(typeNamesList[0]))
- if hasComponent:
- print('Entity has a {} component'.format(typeNamesList[0]))
- return componentOutcome.GetValue()[0]
-
-
-def get_component_property_value(component, component_propertyPath):
- """
- Given a component name and component property path, outputs the property's value.
- :param component: Component object to act on.
- :param componentPropertyPath: String of component property. (e.g. 'Settings|Visible')
- :return: Value set in given componentPropertyPath
- """
- componentPropertyObj = editor.EditorComponentAPIBus(bus.Broadcast, 'GetComponentProperty', component,
- component_propertyPath)
- if componentPropertyObj.IsSuccess():
- componentProperty = componentPropertyObj.GetValue()
- print(f'{component_propertyPath} set to {componentProperty}')
- return componentProperty
- else:
- print(f'FAILURE: Could not get value from {component_propertyPath}')
- return None
-
-
-def get_property_tree(component):
- """
- Given a configured component object, prints the property tree info from that component
- :param component: Component object to act on.
- """
- pteObj = editor.EditorComponentAPIBus(bus.Broadcast, 'BuildComponentPropertyTreeEditor', component)
- pte = pteObj.GetValue()
- print(pte.build_paths_list())
- return pte
-
-
-def compare_values(first_object: object, second_object: object, name: str) -> bool:
- # Quick case - can we just directly compare the two objects successfully?
- if (first_object == second_object):
- result = True
- # No, so get a lot more specific
- elif isinstance(first_object, collections.abc.Container):
- # If they aren't both containers, they're different
- if not isinstance(second_object, collections.abc.Container):
- result = False
- # If they have different lengths, they're different
- elif len(first_object) != len (second_object):
- result = False
- # If they're different strings, they're containers but they failed the == check so
- # we know they're different
- elif isinstance(first_object, str):
- result = False
- else:
- # It's a collection of values, so iterate through them all...
- collection_idx = 0
- result = True
- for val1, val2 in zip(first_object, second_object):
- result = result and compare_values(val1, val2, f"{name} (index [{collection_idx}])")
- collection_idx = collection_idx + 1
-
- else:
- # Do approximate comparisons for floats
- if isinstance(first_object, float) and isclose(first_object, second_object, rel_tol=0.001):
- result = True
- # We currently don't have a generic way to compare PythonProxyObject contents, so return a
- # false positive result for now.
- elif isinstance(first_object, azlmbr.object.PythonProxyObject):
- print(f"{name}: validation inconclusive, the two objects cannot be directly compared.")
- result = True
- else:
- result = False
-
- if not result:
- print(f"compare_values failed: {first_object} ({type(first_object)}) vs {second_object} ({type(second_object)})")
-
- print(f"{name}: {'SUCCESS' if result else 'FAILURE'}")
- return result
-
-
-class Entity:
- """
- Entity class used to create entity objects
- :param name: String for the name of the Entity
- :param id: The ID of the entity
- """
-
- def __init__(self, name: str, id: object = entity.EntityId()):
- self.name: str = name
- self.id: object = id
- self.components: List[object] = None
- self.parent_id = None
- self.parent_name = None
-
- def create_entity(self, entity_position, components, parent_id=entity.EntityId()):
- self.id = editor.ToolsApplicationRequestBus(
- bus.Broadcast, "CreateNewEntityAtPosition", entity_position, entity.EntityId()
- )
- if self.id.IsValid():
- print(f"{self.name} Entity successfully created")
- editor.EditorEntityAPIBus(bus.Event, 'SetName', self.id, self.name)
- self.components = []
- for component in components:
- new_component = add_component(component, self.id)
- self.components.append(new_component)
-
- def get_parent_info(self):
- """
- Sets the value for parent_id and parent_name on the entity (self)
- Prints the string for papertrail
- :return: None
- """
- self.parent_id = editor.EditorEntityInfoRequestBus(bus.Event, "GetParent", self.id)
- self.parent_name = editor.EditorEntityInfoRequestBus(bus.Event, "GetName", self.parent_id)
- print(f"The parent entity of {self.name} is {self.parent_name}")
-
- def set_test_parent_entity(self, parent_entity_obj):
- editor.EditorEntityAPIBus(bus.Event, "SetParent", self.id, parent_entity_obj.id)
- self.get_parent_info()
-
- def get_set_test(self, component_index: int, path: str, value: object, expected_result: object = None) -> bool:
- """
- Used to set and validate changes in component values
- :param component_index: Index location in the self.components list
- :param path: asset path in the component
- :param value: new value for the variable being changed in the component
- :param expected_result: (optional) check the result against a specific expected value
- """
-
- if expected_result is None:
- expected_result = value
-
- # Test Get/Set (get old value, set new value, check that new value was set correctly)
- print(f"Entity {self.name} Path {path} Component Index {component_index} ")
-
- component = self.components[component_index]
- old_value = get_component_property_value(component, path)
-
- if old_value is not None:
- print(f"SUCCESS: Retrieved property Value for {self.name}")
- else:
- print(f"FAILURE: Failed to find value in {self.name} {path}")
- return False
-
- if old_value == expected_result:
- print((f"WARNING: get_set_test on {self.name} is setting the same value that already exists ({old_value})."
- "The set results will be inconclusive."))
-
- editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", component, path, value)
-
- new_value = get_component_property_value(self.components[component_index], path)
-
- if new_value is not None:
- print(f"SUCCESS: Retrieved new property Value for {self.name}")
- else:
- print(f"FAILURE: Failed to find new value in {self.name}")
- return False
-
- return compare_values(new_value, expected_result, f"{self.name} {path}")
-
-
-def get_set_test(entity: object, component_index: int, path: str, value: object) -> bool:
- """
- Used to set and validate changes in component values
- :param component_index: Index location in the entity.components list
- :param path: asset path in the component
- :param value: new value for the variable being changed in the component
- """
- return entity.get_set_test(component_index, path, value)
-
-
-def get_set_property_test(ly_object: object, attribute_name: str, value: object, expected_result: object = None) -> bool:
- """
- Used to set and validate BehaviorContext property changes in Lumberyard objects
- :param ly_object: The lumberyard object to test
- :param attribute_name: property (attribute) name in the BehaviorContext
- :param value: new value for the variable being changed in the component
- :param expected_result: (optional) check the result against a specific expected value other than the one set
- """
-
- if expected_result is None:
- expected_result = value
-
- # Test Get/Set (get old value, set new value, check that new value was set correctly)
- print(f"Attempting to set {ly_object.typename}.{attribute_name} = {value} (expected result is {expected_result})")
-
- if hasattr(ly_object, attribute_name):
- print(f"SUCCESS: Located attribute {attribute_name} for {ly_object.typename}")
- else:
- print(f"FAILURE: Failed to find attribute {attribute_name} in {ly_object.typename}")
- return False
-
- old_value = getattr(ly_object, attribute_name)
-
- if old_value is not None:
- print(f"SUCCESS: Retrieved existing value {old_value} for {attribute_name} in {ly_object.typename}")
- else:
- print(f"FAILURE: Failed to retrieve value for {attribute_name} in {ly_object.typename}")
- return False
-
- if old_value == expected_result:
- print((f"WARNING: get_set_test on {attribute_name} is setting the same value that already exists ({old_value})."
- "The 'set' result for the test will be inconclusive."))
-
- setattr(ly_object, attribute_name, expected_result)
-
- new_value = getattr(ly_object, attribute_name)
-
- if new_value is not None:
- print(f"SUCCESS: Retrieved new value {new_value} for {attribute_name} in {ly_object.typename}")
- else:
- print(f"FAILURE: Failed to retrieve value for {attribute_name} in {ly_object.typename}")
- return False
-
- return compare_values(new_value, expected_result, f"{ly_object.typename}.{attribute_name}")
-def has_components(entity_id: object, component_list: list) -> bool:
- """
- Used to verify if a given entity has all the components of components_list. Returns True if all the
- components are present, else False
- :param entity_id: entity id of the entity
- :param component_list: list of component names to be verified
- """
- typeIdsList = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeIdsByEntityType', component_list , entity.EntityType().Game)
- for type_id in typeIdsList:
- if not editor.EditorComponentAPIBus(bus.Broadcast, 'HasComponentOfType', entity_id, type_id):
- return False
- return True
-
-class PathNotFoundError(Exception):
- def __init__(self, path):
- self.path = path
-
- def __str__(self):
- return f"Path \"{self.path}\" not found in Editor Settings"
-
-def get_editor_settings_path_list():
- """
- Get the list of Editor Settings paths
- """
- paths = editor.EditorSettingsAPIBus(bus.Broadcast, 'BuildSettingsList')
- return paths
-
-def get_editor_settings_by_path(path):
- """
- Get the value of Editor Settings based on the path.
- :param path: path to the Editor Settings to get the value
- """
- if path not in get_editor_settings_path_list():
- raise PathNotFoundError(path)
- outcome = editor.EditorSettingsAPIBus(bus.Broadcast, 'GetValue', path)
- if outcome.isSuccess():
- return outcome.GetValue()
- raise RuntimeError(f"GetValue for path '{path}' failed")
-
-def set_editor_settings_by_path(path, value, is_bool = False):
- """
- Set the value of Editor Settings based on the path.
- # NOTE: Some Editor Settings may need an Editor restart to apply.
- # Ex: Enabling or disabling New Viewport Interaction Model
- :param path: path to the Editor Settings to get the value
- :param value: value to be set
- :param is_bool: True for Boolean settings (enable/disable), False for other settings
- """
- if path not in get_editor_settings_path_list():
- raise PathNotFoundError(path)
- if is_bool and not isinstance(value, bool):
- def ParseBoolValue(value):
- if(value == "0"):
- return False
- return True
- value = ParseBoolValue(value)
- outcome = editor.EditorSettingsAPIBus(bus.Broadcast, 'SetValue', path, value)
- if not outcome.isSuccess():
- raise RuntimeError(f"SetValue for path '{path}' failed")
- print(f"Value for path '{path}' is set to {value}")
diff --git a/Tests/ly_shared/hydra_lytt_test_utils.py b/Tests/ly_shared/hydra_lytt_test_utils.py
deleted file mode 100755
index bbdcbc6cb6..0000000000
--- a/Tests/ly_shared/hydra_lytt_test_utils.py
+++ /dev/null
@@ -1,77 +0,0 @@
-"""
-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 logging
-import os
-import tempfile
-import ly_test_tools.log.log_monitor
-import ly_test_tools.environment.process_utils as process_utils
-import ly_test_tools.environment.waiter as waiter
-
-logger = logging.getLogger(__name__)
-
-
-def teardown_editor(editor):
- """
- :param editor: Configured editor object
- :return:
- """
- process_utils.kill_processes_named('AssetProcessor.exe')
- logger.debug('Ensuring Editor is stopped')
- editor.ensure_stopped()
-
-
-def launch_and_validate_results(request, test_directory, editor, editor_script, expected_lines, unexpected_lines=[],
- halt_on_unexpected=False, auto_test_mode=True, run_python="--runpythontest", cfg_args=[], timeout=60, log_creation_max_wait=60):
- """
- Creates a temporary config file for Hydra execution, runs the Editor with the specified script, and monitors for
- expected log lines.
- :param request: Special fixture providing information of the requesting test function.
- :param test_directory: Path to test directory that editor_script lives in.
- :param editor: Configured editor object to run test against.
- :param editor_script: Name of script that will execute in the Editor.
- :param expected_lines: Expected lines to search log for.
- :param unexpected_lines: Unexpected lines to search log for. Defaults to none.
- :param halt_on_unexpected: Halts test if unexpected lines are found. Defaults to False.
- :param auto_test_mode: Defaults to True. Runs the test in auto_test_mode.
- :param run_python: Defaults to "--runpythontest", other option is "--runpython".
- :param cfg_args: Additional arguments for CFG, such as LevelName.
- :param timeout: Length of time for test to run. Default is 60.
- :param log_creation_max_wait: Length of time for waiting to find the log file. Default is 60.
- """
- test_case = os.path.join(test_directory, editor_script)
- request.addfinalizer(lambda: teardown_editor(editor))
- logger.debug("Running automated test: {}".format(editor_script))
-
- editor.args.extend(["--skipWelcomeScreenDialog"])
- if auto_test_mode: editor.args.extend(["--autotest_mode"])
- editor.args.extend([run_python, test_case, "--runpythonargs", cfg_args])
-
- with editor.start():
-
- editorlog_file = os.path.join(editor.workspace.paths.project_log(), 'Editor.log')
- log_monitor = ly_test_tools.log.log_monitor.LogMonitor(launcher=editor, log_file_path=editorlog_file, log_creation_max_wait_time=log_creation_max_wait)
- log_monitor.monitor_log_for_lines(expected_lines=expected_lines, unexpected_lines=unexpected_lines,
- halt_on_unexpected=halt_on_unexpected, timeout=timeout)
-
-
-def remove_files(artifact_path, suffix):
- """
- Removes files with the specified suffix from the specified path
- :param artifact_path: Path to search for files
- :param suffix: File extension to remove
- """
- if not os.path.isdir(artifact_path):
- return
-
- for file_name in os.listdir(artifact_path):
- if file_name.endswith(suffix):
- os.remove(os.path.join(artifact_path, file_name))
diff --git a/Tests/ly_shared/network_utils.py b/Tests/ly_shared/network_utils.py
deleted file mode 100755
index 8b772303f1..0000000000
--- a/Tests/ly_shared/network_utils.py
+++ /dev/null
@@ -1,66 +0,0 @@
-"""
-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 logging
-import psutil
-import socket
-
-
-logger = logging.getLogger(__name__)
-
-
-def check_for_listening_port(port):
- """
- Checks to see if the connection to the designated port was established.
- :param port: Port to listen to.
- :return: True if port is listening.
- """
- port_listening = False
- for conn in psutil.net_connections():
- if 'port={}'.format(port) in str(conn):
- port_listening = True
- return port_listening
-
-
-def check_for_remote_listening_port(port, ip_addr='127.0.0.1'):
- """
- Tries to connect to a port to see if port is listening.
- :param port: Port being tested.
- :param ip_addr: IP address of the host being connected to.
- :return: True if connection to the port is established.
- """
- port_listening = True
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- try:
- sock.connect((ip_addr, port))
- except socket.error as err:
- # Socket error: Connection refused, error code 10061
- if err.errno == 10061:
- port_listening = False
- finally:
- sock.close()
- return port_listening
-
-
-def get_local_ip_address():
- """
- Finds the IP address for the primary ethernet adapter by opening a connection and grabbing its IP address.
- :return: The IP address for the adapter used to make the connection.
- """
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- try:
- # Connecting to Google's public DNS so there is an open connection
- # and then getting the address used for that connection
- sock.connect(('8.8.8.8', 80))
- host_ip = sock.getsockname()[0]
- finally:
- sock.close()
- return host_ip
diff --git a/Tests/ly_shared/phase.py b/Tests/ly_shared/phase.py
deleted file mode 100755
index 4bb827b3ad..0000000000
--- a/Tests/ly_shared/phase.py
+++ /dev/null
@@ -1,198 +0,0 @@
-"""
-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 represents one "phase" of a test. The test runner communicates with the launcher by running phases and waiting
-for results, however they appear.
-"""
-
-import logging
-import os
-import time
-import xml.etree.ElementTree
-
-import ly_test_tools.launchers.exceptions as exceptions
-from ly_test_tools.launchers.platforms.base import Launcher
-
-_POLL_INTERVAL_SEC = 1
-_CRASH_TIMEOUT = 5
-
-logger = logging.getLogger(__name__)
-
-
-class Phase(object):
- """
- A generic test phase for running the launcher. With the launcher running elsewhere (at a minimum in a different
- process, possibly on a different device altogether) the following kind of phases might occur:
-
- - Wait for a specific file to show up / complete processing.
- - Send commands to the launcher over the network and wait for response.
- - Wait for multiple launchers to coordinate networking testing.
- - Wait for a specific amount of time.
-
- Each phase can then compile the artifacts for the next phase.
- """
- def __init__(self, timeout):
- """
- :param timeout: Maximum time allocated for phase.
- """
- self.timeout = timeout
-
- def _start(self, previous_phase=None):
- """
- Start the phase.
-
- :return: None
- """
- logger.debug("start: {}".format(self.__class__.__name__))
-
- def _is_complete(self):
- """
- Check if the phase is complete. This is the only required function.
-
- :return: None
- """
- raise NotImplementedError
-
- def _compile_artifacts(self):
- """
- Compile artifacts after a completed phase.
- """
- logger.debug("compile_artifacts: {}".format(self.__class__.__name__))
-
- def _update(self, elapsed_time):
- """
- Update the test phase if necessary.
-
- :param elapsed_time: Time since the last update.
- :return: None
- """
- logger.debug("update: {}".format(self.__class__.__name__))
-
- def _wait(self, launcher):
- """
- Wait for the phase to complete.
-
- :return: None.
- :raises: TimeoutError, CrashError
- """
- dead_time = -1
- logger.debug("wait begin: {}".format(self.__class__.__name__))
- start = time.time()
-
- while not self._is_complete():
-
- if time.time() - start > self.timeout:
- message = "Timeout exceeded {}s in {}".format(self.timeout, self.__class__.__name__)
- logger.error(message)
- raise exceptions.TimeoutError(message)
- elif not launcher.is_alive():
- # The final result may arrive after the app closes.
- if dead_time == -1:
- dead_time = time.time()
- if time.time() - dead_time > _CRASH_TIMEOUT:
- message = "Unexpected termination in {} after {:0.2f}s".format(
- self.__class__.__name__, time.time() - start)
- logger.error(message)
- raise exceptions.CrashError(message)
-
- sleep_start = time.time()
- time.sleep(_POLL_INTERVAL_SEC)
-
- self._update(time.time() - sleep_start)
-
- logger.debug("wait end: {}, duration: {:.2f}".format(self.__class__.__name__, time.time() - start))
-
-
-class FileExistsPhase(Phase):
- """
- Test phase that completes when a specific file is created.
- """
- def __init__(self, path, timeout=60, non_empty=False):
- super(FileExistsPhase, self).__init__(timeout)
- self.path = path
- self.non_empty = non_empty
-
- def _is_complete(self):
- if self.path is not None and os.path.exists(self.path):
- if self.non_empty:
- return os.path.getsize(self.path) > 0
- else:
- return True
-
- return False
-
-
-class XMLValidPhase(FileExistsPhase):
- """
- Test phase that completes when a valid XML file is found.
- """
- def __init__(self, path, timeout=60):
- super(XMLValidPhase, self).__init__(path, timeout, non_empty=True)
- self.path = path
- self.xml = None
-
- def _is_complete(self):
- if not super(XMLValidPhase, self)._is_complete():
- return False
-
- try:
- self.xml = xml.etree.ElementTree.parse(self.path)
- except xml.etree.ElementTree.ParseError:
- return False
-
- return True
-
-
-class TimePhase(Phase):
- """
- Simple class to complete in a specified time. Can be used to test timeout.
- """
- def __init__(self, timeout, complete_time):
- super(TimePhase, self).__init__(timeout)
- self.complete_time = complete_time
-
- def _start(self, previous_phase=None):
- super(TimePhase, self)._start(previous_phase)
- self.start_time = time.time()
-
- def _is_complete(self):
- return time.time() - self.start_time > self.complete_time
-
-
-class ElapsedTimePhase(Phase):
- """
- Simple class to complete in a specified time using elapsed time. Can be used to test timeout and elapsed time.
- """
- def __init__(self, timeout, complete_time):
- super(ElapsedTimePhase, self).__init__(timeout)
- self.complete_time = complete_time
- self.total_time = None
-
- def _start(self, previous_phase=None):
- super(ElapsedTimePhase, self)._start(previous_phase)
- self.total_time = 0
-
- def _update(self, elapsed_time):
- super(ElapsedTimePhase, self)._update(elapsed_time)
- self.total_time += elapsed_time
-
- def _is_complete(self):
- return self.total_time > self.complete_time
-
-
-class WaitForLauncherToQuit(Phase):
- def __init__(self, launcher, timeout=60):
- # type: (Launcher, int) -> None
- super(WaitForLauncherToQuit, self).__init__(timeout)
- self.launcher = launcher
-
- def _is_complete(self):
- # type: () -> bool
- return not self.launcher.is_alive()
diff --git a/Tests/ly_shared/pyside_utils.py b/Tests/ly_shared/pyside_utils.py
deleted file mode 100755
index 1dfdcdb5c0..0000000000
--- a/Tests/ly_shared/pyside_utils.py
+++ /dev/null
@@ -1,939 +0,0 @@
-"""
-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.qt
-import azlmbr.qt_helpers
-import asyncio
-import re
-from shiboken2 import wrapInstance, getCppPointer
-from PySide2 import QtCore, QtWidgets, QtGui, QtTest
-from PySide2.QtWidgets import QAction, QWidget
-from PySide2.QtCore import Qt
-from PySide2.QtTest import QTest
-import azlmbr.legacy.general as general
-import traceback
-import threading
-import types
-
-
-qApp = QtWidgets.QApplication.instance()
-# Monkey patch static method calls
-QtWidgets.QApplication.activeModalWidget = qApp.activeModalWidget
-
-
-class LmbrQtEventLoop(asyncio.AbstractEventLoop):
- def __init__(self):
- self.running = False
- self.shutdown = threading.Event()
- self.blocked_events = set()
- self.finished_events = set()
- self.queue = []
- self._wait_future = None
- self._event_loop_nesting = 0
-
- def get_debug(self):
- return False
-
- def time(self):
- return azlmbr.qt_helpers.time()
-
- def wait_for_condition(self, condition, action, on_timeout=None, timeout=1.0):
- timeout = self.time() + timeout if timeout is not None else None
- def callback(time):
- # Run our action and remove us from the queue if our condition is satisfied
- if condition():
- action()
- return True
- # Give up if timeout has elapsed
- if time > timeout:
- if on_timeout is not None:
- on_timeout()
- return True
- return False
- self.queue.append((callback))
-
- def event_loop(self):
- time = self.time()
- def run_event(event):
- if event in self.blocked_events or event in self.finished_events:
- return False
- self.blocked_events.add(event)
- try:
- if event(time):
- self.finished_events.add(event)
- except Exception:
- traceback.print_exc()
- self.finished_events.add(event)
- finally:
- self.blocked_events.remove(event)
-
- self._event_loop_nesting += 1
- try:
- for event in self.queue:
- run_event(event)
- finally:
- self._event_loop_nesting -= 1
-
- # Clear out any finished events if the queue is safe to mutate
- if self._event_loop_nesting == 0:
- self.queue = [event for event in self.queue if event not in self.finished_events]
- self.finished_events = set()
-
- if not self.running or self._wait_future is not None and self._wait_future.done():
- self.close()
-
- def run_until_shutdown(self):
- # Run our event loop callback (via azlmbr.qt_helpers) by pumping the Qt event loop
- # azlmbr.qt_helpers will attempt to ensure our event loop is always run, even when a
- # new event loop is started and run from the main event loop
- self.running = True
- self.shutdown.clear()
- azlmbr.qt_helpers.set_loop_callback(self.event_loop)
- while not self.shutdown.is_set():
- qApp.processEvents(QtCore.QEventLoop.AllEvents, 0)
-
- def run_forever(self):
- self._wait_future = None
- self.run_until_shutdown()
-
- def run_until_complete(self, future):
- # Wrap coroutines into Tasks (future-like analogs)
- if isinstance(future, types.CoroutineType):
- future = self.create_task(future)
- self._wait_future = future
- self.run_until_shutdown()
-
- def _timer_handle_cancelled(self, handle):
- pass
-
- def is_running(self):
- return self.running
-
- def is_closed(self):
- return not azlmbr.qt_helpers.loop_is_running()
-
- def stop(self):
- self.running = False
-
- def close(self):
- self.running = False
- self.shutdown.set()
- azlmbr.qt_helpers.clear_loop_callback()
-
- def shutdown_asyncgens(self):
- pass
-
- def call_exception_handler(self, context):
- try:
- raise context.get('exception', None)
- except:
- traceback.print_exc()
-
- def call_soon(self, callback, *args, **kw):
- h = asyncio.Handle(callback, args, self)
- def callback_wrapper(time):
- if not h.cancelled():
- h._run()
- return True
- self.queue.append(callback_wrapper)
- return h
-
- def call_later(self, delay, callback, *args, **kw):
- if delay < 0:
- raise Exception("Can't schedule in the past")
- return self.call_at(self.time() + delay, callback, *args)
-
- def call_at(self, when, callback, *args, **kw):
- h = asyncio.TimerHandle(when, callback, args, self)
- h._scheduled = True
- def callback_wrapper(time):
- if time > when:
- if not h.cancelled():
- h._run()
- return True
- return False
- self.queue.append(callback_wrapper)
- return h
-
- def create_task(self, coro):
- return asyncio.Task(coro, loop=self)
-
- def create_future(self):
- return asyncio.Future(loop=self)
-
-
-class EventLoopTimeoutException(Exception):
- pass
-
-
-event_loop = LmbrQtEventLoop()
-def wait_for_condition(condition, timeout=1.0):
- """
- Asynchronously waits for `condition` to evaluate to True.
- condition: A function with the signature def condition() -> bool
- This condition will be evaluated until it evaluates to True or the timeout elapses
- timeout: The time in seconds to wait - if 0, this will wait forever
- Throws pyside_utils.EventLoopTimeoutException on timeout.
- """
- future = event_loop.create_future()
- def on_complete():
- future.set_result(True)
- def on_timeout():
- future.set_exception(EventLoopTimeoutException())
- event_loop.wait_for_condition(condition, on_complete, on_timeout=on_timeout, timeout=timeout)
- return future
-
-
-async def wait_for(expression, timeout=1.0):
- """
- Asynchronously waits for "expression" to evaluate to a non-None value,
- then returns that value.
-
- expression: A function with the signature def expression() -> Generic[Any,None]
- The result of expression will be returned as soon as it returns a non-None value.
- timeout: The time in seconds to wait - if 0, this will wait forever
- Throws pyside_utils.EventLoopTimeoutException on timeout.
- """
- result = None
- def condition():
- nonlocal result
- result = expression()
- return result is not None
- await wait_for_condition(condition, timeout)
- return result
-
-
-def run_soon(fn):
- """
- Runs a function on the event loop to enable asynchronous execution.
-
- fn: The function to run, should be a function that takes no arguments
- Returns a future that will be popualted with the result of fn or the exception it threw.
- """
- future = event_loop.create_future()
- def coroutine():
- try:
- fn()
- future.set_result(True)
- except Exception as e:
- future.set_exception(e)
- event_loop.call_soon(coroutine)
- return future
-
-
-def run_async(awaitable):
- """
- Synchronously runs a coroutine or a future on the event loop.
- This can be used in lieu of "await" in non-async functions.
-
- awaitable: The coroutine or future to await.
- Returns the result of operation specified.
- """
- if isinstance(awaitable, types.CoroutineType):
- awaitable = event_loop.create_task(awaitable)
- event_loop.run_until_complete(awaitable)
- return awaitable.result()
-
-
-def wrap_async(fn):
- """
- This decorator enables an async function's execution from a synchronous one.
-
- For example:
- @pyside_utils.wrap_async
- async def foo():
- result = await long_operation()
- return result
-
- def non_async_fn():
- x = foo() # this will return the correct result by executing the event loop
-
- fn: The function to wrap
- Returns the decorated function.
- """
- def wrapper(*args, **kw):
- result = fn(*args, **kw)
- return run_async(result)
- return wrapper
-
-
-def get_editor_main_window():
- """
- Fetches the main Editor instance of QMainWindow for use with PySide tests
- :return Instance of QMainWindow for the Editor
- """
- params = azlmbr.qt.QtForPythonRequestBus(azlmbr.bus.Broadcast, "GetQtBootstrapParameters")
- editor_id = QtWidgets.QWidget.find(params.mainWindowId)
- main_window = wrapInstance(int(getCppPointer(editor_id)[0]), QtWidgets.QMainWindow)
- return main_window
-
-
-def get_action_for_menu_path(editor_window: QtWidgets.QMainWindow, main_menu_item: str, *menu_item_path: str):
- """
- main_menu_item: Main menu item among the MenuBar actions. Ex: "File"
- menu_item_path: Path to any nested menu item. Ex: "Viewport", "Goto Coordinates"
- returns: QAction object for the corresponding path.
- """
- # Check if path is valid
- menu_bar = editor_window.menuBar()
- menu_bar_actions = [index.iconText() for index in menu_bar.actions()]
-
- # Verify if the given Menu exists in the Menubar
- if main_menu_item not in menu_bar_actions:
- print(f"QAction not found for main menu item '{main_menu_item}'")
- return None
- curr_action = menu_bar.actions()[menu_bar_actions.index(main_menu_item)]
- curr_menu = curr_action.menu()
- for index, element in enumerate(menu_item_path):
- curr_menu_actions = [index.iconText() for index in curr_menu.actions()]
- if element not in curr_menu_actions:
- print(f"QAction not found for menu item '{element}'")
- return None
- if index == len(menu_item_path) - 1:
- return curr_menu.actions()[curr_menu_actions.index(element)]
- curr_action = curr_menu.actions()[curr_menu_actions.index(element)]
- curr_menu = curr_action.menu()
- return None
-
-
-def _pattern_to_dict(pattern, **kw):
- """
- Helper function, turns a pattern match parameter into a normalized dictionary
- """
-
- def is_string_or_regex(x):
- return isinstance(x, str) or isinstance(x, re.Pattern)
-
- # If it's None, just make an empty dict
- if pattern is None:
- pattern = {}
- # If our pattern is a string or regex, turn it into a text match
- elif is_string_or_regex(pattern):
- pattern = dict(text=pattern)
- # If our pattern is an (int, int) tuple, turn it into a row/column match
- elif isinstance(pattern, tuple) and isinstance(pattern[0], int) and isinstance(pattern[1], int):
- pattern = dict(row=pattern[0], column=pattern[1])
- # If our pattern is a QObject type, turn it into a type match
- elif isinstance(pattern, type(QtCore.QObject)):
- pattern = dict(type=pattern)
- # Otherwise assume it's a dict and make a copy
- else:
- pattern = dict(pattern)
-
- # Merge with any kw arguments
- for key, value in kw.items():
- pattern[key] = value
- return pattern
-
-
-def _match_pattern(obj, pattern):
- """
- Helper function, determines whether obj matches the pattern specified by pattern.
-
- It is required that pattern is normalized into a dict before calling this.
- """
-
- def compare(value1, value2):
- # Do a regex search if it's a regex, otherwise do a normal compare
- if isinstance(value2, re.Pattern):
- return re.search(value2, value1)
- return value1 == value2
-
- item_roles = Qt.ItemDataRole.values.values()
- for key, value in pattern.items():
- if key == "type": # Class type
- if not isinstance(obj, value):
- return False
- elif key == "text": # Default 'text' path, depends on type
- text_values = []
-
- def get_from_attrs(*args):
- for attr in args:
- try:
- text_values.append(getattr(obj, attr)())
- except Exception:
- pass
-
- # Use any of the following fields for default matching, if they're defined
- get_from_attrs("text", "objectName", "windowTitle")
- # Additionally, use the DisplayRole for QModelIndexes
- if isinstance(obj, QtCore.QModelIndex):
- text_values.append(obj.data(Qt.DisplayRole))
-
- if not any(compare(text, value) for text in text_values):
- return False
- elif key in item_roles: # QAbstractItemModel display role
- if not isinstance(obj, QtCore.QModelIndex):
- raise RuntimeError(f"Attempted to match data role on unsupported object {obj}")
- if not compare(obj.data(key), value):
- return False
- elif hasattr(obj, key):
- # Look up our key on the object itself
- objectValue = getattr(obj, key)
- # Invoke it if it's a getter
- if callable(objectValue):
- objectValue = objectValue()
- if not compare(objectValue, value):
- return False
- else:
- return False
-
- return True
-
-
-def get_child_indexes(model, parent_index=QtCore.QModelIndex()):
- indexes = [parent_index]
- while len(indexes) > 0:
- parent_index = indexes.pop(0)
- for row in range(model.rowCount(parent_index)):
- # FIXME
- # PySide appears to have a bug where-in it thinks columnCount is private
- # Bail gracefully for now, we can add a C++ wrapper to work around if needed
- try:
- column_count = model.columnCount(parent_index)
- except Exception:
- column_count = 1
- for col in range(column_count):
- cur_index = model.index(row, col, parent_index)
- yield cur_index
-
-
-def _get_children(obj):
- """
- Helper function. Get the direct descendants from a given PySide object.
- This includes all: QObject children, QActions owned by the object, and QModelIndexes if applicable
- """
- if isinstance(obj, QtCore.QObject):
- yield from obj.children()
- if isinstance(obj, QtWidgets.QWidget):
- yield from obj.actions()
- if isinstance(obj, (QtWidgets.QAbstractItemView, QtCore.QModelIndex)):
- model = obj.model()
- if model is None:
- return
-
- # For a QAbstractItemView (e.g. QTreeView, QListView), the parent index
- # will be an invalid QModelIndex(), which will use find all indexes on the root.
- # For a QModelIndex, we use the actual QModelIndex as the parent_index so that
- # it will find any child indexes under it
- parent_index = QtCore.QModelIndex()
- if isinstance(obj, QtCore.QModelIndex):
- parent_index = obj
-
- yield from get_child_indexes(model, parent_index)
-
-
-def _get_parents_to_search(obj_entry_or_list):
- """
- Helper function, turns obj_entry_or_list into a list of parents to search
-
- If obj_entry_or_list is None, returns all visible top level widgets
- If obj_entry_or_list is iterable, return it as a list
- Otherwise, return a list containing obj_entry_or_list
- """
- if obj_entry_or_list is None:
- return [widget for widget in QtWidgets.QApplication.topLevelWidgets() if widget.isVisible()]
- try:
- return list(obj_entry_or_list)
- except TypeError:
- return [obj_entry_or_list]
-
-
-def find_children_by_pattern(obj=None, pattern=None, recursive=True, **kw):
- """
- Finds the children of an object that match a given pattern.
- See find_child_by_pattern for more information on usage.
- """
- pattern = _pattern_to_dict(pattern, **kw)
- parents_to_search = _get_parents_to_search(obj)
-
- while len(parents_to_search) > 0:
- parent = parents_to_search.pop(0)
- for child in _get_children(parent):
- if _match_pattern(child, pattern):
- yield child
- if recursive:
- parents_to_search.append(child)
-
-
-def find_child_by_pattern(obj=None, pattern=None, recursive=True, **kw):
- """
- Finds the child of an object that matches a given pattern.
- A "child" in this context is not necessarily a QObject child.
- QActions are also considered children, as are the QModelIndex children of QAbstractItemViews.
- obj: The object to search - should be either a QObject or a QModelIndex, or a list of them
- If None this will search all top level windows.
- pattern: The pattern to match, the first child that matches all of the criteria specified will
- be returned. This is a dictionary with any combination of the following:
-
- - "text": generic text to match, will search object names for QObjects, display role text
- for QModelIndexes, or action text() for QActions
- - "type": a class type, e.g. QtWidgets.QMenu, a child will only match if it's of this type
- - "row" / "column": integer row and column indices of a QModelIndex
- - "type": type class (e.g. PySide.QtWidgets.QComboBox) that the object must inherit from
- - A Qt.ItemDataRole: matches for QModelIndexes with data of a given value
- - Any other fields will fall back on being looked up on the object itself by name, e.g.
- {"windowTitle": "Foo"} would match a windowTitle named "Foo"
-
- Any instances where a field is specified as text can also be specified as a regular expression:
- find_child_by_pattern(obj, {text: re.compile("Foo_.*")}) would find a child with text starting
- with "Foo_"
-
- For convenience, these parameter types may also be specified as keyword arguments:
- find_child_by_pattern(obj, text="foo", type=QtWidgets.QAction)
- is equivalent to
- find_child_by_pattern(obj, {"text": "foo", "type": QtWidgets.QAction})
-
- If pattern is specified as a string, it will turn into a pattern matching "text":
- find_child_by_pattern(obj, "foo")
- is equivalent to
- find_child_by_pattern(obj, {"text": "foo"})
-
- If a pattern is specified as an (int, int) tuple, it will turn into a row/column match:
- find_child_by_pattern(obj, (0, 2))
- is equivalent to
- find_child_by_pattern(obj, {"row": 0, "column": 2})
-
- If a pattern is specified as a type, like PySide.QtWidgets.QLabel, it will turn into a type match:
- find_child_by_pattern(obj, PySide.QtWidgets.QLabel)
- is equivalent to
- find_child_by_pattern(obj, {"type": PySide.QtWidgets.QLabel})
- """
- # Return the first match result, if found
- for match in find_children_by_pattern(obj, pattern=pattern, recursive=recursive, **kw):
- return match
- return None
-
-
-def find_child_by_hierarchy(parent, *patterns):
- """
- Searches for a hierarchy of children descending from parent.
- parent: The Qt object (or list of Qt obejcts) to search within
- If none, this will search all top level windows.
- patterns: A list of patterns to match to find a hierarchy of descendants.
- These patterns will be tested in order.
-
- For example, to look for the QComboBox in a hierarchy like the following:
- QWidget (window)
- -QTabWidget
- -QWidget named "m_exampleTab"
- -QComboBox
- One might invoke:
- find_child_by_hierarchy(window, QtWidgets.QTabWidget, "m_exampleTab", QtWidgets.QComboBox)
-
- Alternatively, "..." may be specified in place of a parent, where the hierarchy will match any
- ancestors along the path, so the above might be shortened to:
- find_child_by_hierarchy(window, ..., "m_exampleTab", QtWidgets.QComboBox)
- """
- search_recursively = False
- current_objects = _get_parents_to_search(parent)
- for pattern in patterns:
- # If it's an ellipsis, do the next search recursively as we're looking for any number of intermediate ancestors
- if pattern is ...:
- search_recursively = True
- continue
-
- candidates = []
- for parent_candidate in current_objects:
- candidates += find_children_by_pattern(parent_candidate, pattern=pattern, recursive=search_recursively)
- if len(candidates) == 0:
- return None
- current_objects = candidates
-
- search_recursively = False
- return current_objects[0]
-
-async def wait_for_child_by_hierarchy(parent, *patterns, timeout=1.0):
- """
- Searches for a hierarchy of children descending from parent until timeout occurs.
- Returns a future that will result in either the found child or an EventLoopTimeoutException.
-
- See find_child_by_hierarchy for usage information.
- """
- match = None
- def condition():
- nonlocal match
- match = find_child_by_hierarchy(parent, *patterns)
- return match is not None
- await wait_for_condition(condition, timeout)
- return match
-
-
-async def wait_for_child_by_pattern(obj=None, pattern=None, recursive=True, timeout=1.0, **kw):
- """
- Finds the child of an object that matches a given pattern.
- Returns a future that will result in either the found child or an EventLoopTimeoutException.
-
- See find_child_by_hierarchy for usage information.
- """
- match = None
- def condition():
- nonlocal match
- match = find_child_by_pattern(obj, pattern, recursive, **kw)
- return match is not None
- await wait_for_condition(condition, timeout)
- return match
-
-
-def find_child_by_property(obj, obj_type, property_name, property_value, reg_exp_search=False):
- """
- Finds the child of an object which has the property name matching the property value
- of type obj_type
- obj: The property value is searched through obj children
- obj_type: Type of object to be matched
- property_name: Property of the child which should be verified for the required value.
- property_value: Property value that needs to be matched
- reg_exp_search: If True searches for the property_value based on re search. Defaults to False.
- """
- for child in obj.children():
- if reg_exp_search and re.search(property_value, getattr(child, property_name)()):
- return child
- if not reg_exp_search and isinstance(child, obj_type) and getattr(child, property_name)() == property_value:
- return child
- return None
-
-def get_item_view_index(item_view, row, column=0, parent=QtCore.QModelIndex()):
- """
- Retrieve the index for a specified row/column, with optional parent
- This is necessary when needing to reference into nested hierarchies in a QTreeView
- item_view: The QAbstractItemView instance
- row: The requested row index
- column: The requested column index (defaults to 0 in case of single column)
- parent: Parent index (defaults to invalid)
- """
- item_model = item_view.model()
- model_index = item_model.index(row, column, parent)
- return model_index
-
-
-def get_item_view_index_rect(item_view, index):
- """
- Gets the QRect for a given index in a QAbstractItemView (e.g. QTreeView, QTableView, QListView).
- This is helpful because for sending mouse events to a QAbstractItemView, you have to send them to
- the viewport() widget of the QAbstractItemView.
- item_view: The QAbstractItemView instance
- index: A QModelIndex for the item index
- """
- return item_view.visualRect(index)
-
-
-def item_view_index_mouse_click(item_view, index, button=QtCore.Qt.LeftButton, modifier=QtCore.Qt.NoModifier):
- """
- Helper method version of QTest.mouseClick for injecting mouse clicks on a QAbstractItemView
- item_view: The QAbstractItemView instance
- index: A QModelIndex for the item index to be clicked
- """
- item_index_rect = get_item_view_index_rect(item_view, index)
- item_index_center = item_index_rect.center()
-
- # For QAbstractItemView widgets, the events need to be forwarded to the actual viewport() widget
- QTest.mouseClick(item_view.viewport(), button, modifier, item_index_center)
-
-
-def item_view_mouse_click(item_view, row, column=0, button=QtCore.Qt.LeftButton, modifier=QtCore.Qt.NoModifier):
- """
- Helper method version of 'item_view_index_mouse_click' using a row, column instead of a QModelIndex
- item_view: The QAbstractItemView instance
- row: The requested row index
- column: The requested column index (defaults to 0 in case of single column)
- """
- index = get_item_view_index(item_view, row, column)
- item_view_index_mouse_click(item_view, index, button, modifier)
-
-
-async def wait_for_action_in_menu(menu, pattern, timeout=1.0):
- """
- Finds a QAction inside a menu, based on the specified pattern.
-
- menu: The QMenu to search
- pattern: The action text or pattern to match (see find_child_by_pattern)
- If pattern specifies a QWidget, this will search for the associated QWidgetAction
- """
- action = await wait_for_child_by_pattern(menu, pattern, timeout=timeout)
- if action is None:
- raise TimeoutError(f"Failed to find context menu action for {pattern}")
-
- # If we've found a valid QAction, we're good to go
- if hasattr(action, 'trigger'):
- return action
-
- # If pattern matches a widget and not a QAction, look for an associated QWidgetAction
- widget_actions = find_children_by_pattern(menu, type=QtWidgets.QWidgetAction)
- underlying_widget_action = None
- for widget_action in widget_actions:
- widgets_to_check = [widget_action.defaultWidget()] + widget_action.createdWidgets()
- for check_widget in widgets_to_check:
- if action in _get_children(check_widget):
- underlying_widget_action = widget_action
- break
- if underlying_widget_action is not None:
- action = underlying_widget_action
- break
-
- if not hasattr(action, 'trigger'):
- raise RuntimeError(f"Failed to find action associated with widget {action}")
- return action
-
-
-def queue_hide_event(widget):
- """
- Explicitly post a hide event for the next frame, this can be used to ensure modal dialogs exit correctly.
-
- widget: The widget to hide
- """
- qApp.postEvent(widget, QtGui.QHideEvent())
-
-
-async def wait_for_destroyed(obj, timeout=1.0):
- """
- Waits for a QObject (including a widget) to be fully destroyed
-
- This can be used to wait for a modal dialog to shut down properly
-
- obj: The object to wait on destruction
- timeout: The time, in seconds to wait. 0 for an indefinite wait.
- """
- was_destroyed = False
- def on_destroyed():
- nonlocal was_destroyed
- was_destroyed = True
- obj.destroyed.connect(on_destroyed)
- return await wait_for_condition(lambda: was_destroyed, timeout=timeout)
-
-
-async def close_modal(modal_widget, timeout=1.0):
- """
- Closes a modal dialog and waits for it to be cleaned up.
-
- This attempts to ensure the modal event loop gets properly exited.
-
- modal_widget: The widget to close
- timeout: The time, in seconds, to wait. 0 for an indefinite wait.
- """
- queue_hide_event(modal_widget)
- return await wait_for_destroyed(modal_widget, timeout=timeout)
-
-
-def trigger_context_menu_entry(widget, pattern, pos=None, index=None):
- """
- Trigger a context menu event on a widget and activate an entry
- widget: The widget to trigger the event on
- pattern: The action text or pattern to match (see find_child_by_pattern)
- pos: Optional, the QPoint to set as the event origin
- index: Optional, the QModelIndex to click in widget
- widget must be a QAbstractItemView
- """
- async def async_wrapper():
- menu = await open_context_menu(widget, pos=pos, index=index)
- action = await wait_for_action_in_menu(menu, pattern)
- action.trigger()
- queue_hide_event(menu)
-
- result = async_wrapper()
- # If we have an event loop, go ahead and just return the coroutine
- # Otherwise, do a synchronous wait
- if event_loop.is_running():
- return result
- else:
- return run_async(result)
-
-
-async def open_context_menu(widget, pos=None, index=None, timeout=1.0):
- """
- Trigger a context menu event on a widget
- widget: The widget to trigger the event on
- pos: Optional, the QPoint to set as the event origin
- index: Optional, the QModelIndex to click in widget
- widget must be a QAbstractItemView
-
- Returns the menu that was created.
- """
- if index is not None:
- if pos is not None:
- raise RuntimeError("Error: 'index' and 'pos' are mutually exclusive")
- pos = widget.visualRect(index).center()
- parent = widget
- widget = widget.viewport()
- pos = widget.mapFrom(parent, pos)
- if pos is None:
- pos = widget.rect().center()
-
- # Post both a mouse event and a context menu to let the widget handle whichever is appropriate
- qApp.postEvent(widget, QtGui.QContextMenuEvent(QtGui.QContextMenuEvent.Mouse, pos))
- QtTest.QTest.mouseClick(widget, Qt.RightButton, Qt.NoModifier, pos)
-
- menu = None
- # Wait for a menu popup
- def menu_has_focus():
- nonlocal menu
- for fw in [qApp.activePopupWidget(), qApp.activeModalWidget(), qApp.focusWidget(), qApp.activeWindow()]:
- if fw and isinstance(fw, QtWidgets.QMenu) and fw.isVisible():
- menu = fw
- return True
- return False
- await wait_for_condition(menu_has_focus, timeout)
- return menu
-
-
-def move_mouse(widget, position):
- """
- Helper method to move the mouse to a specified position on a widget
- widget: The widget to trigger the event on
- position: The QPoint (local to widget) to move the mouse to
- """
- # For some reason, Qt wouldn't register the mouse movement correctly unless both of these ways are invoked.
- # The QTest.mouseMove seems to update the global cursor position, but doesn't always result in the MouseMove event being
- # triggered, which prevents drag/drop being able to be simulated.
- # Similarly, if only the MouseMove event is sent by itself to the core application, the global cursor position wasn't
- # updated properly, so drag/drop logic that depends on grabbing the globalPos didn't work.
- QtTest.QTest.mouseMove(widget, position)
- event = QtGui.QMouseEvent(QtCore.QEvent.MouseMove, position, widget.mapToGlobal(position), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier)
- QtCore.QCoreApplication.sendEvent(widget, event)
-
-
-def drag_and_drop(source, target, source_point = QtCore.QPoint(), target_point = QtCore.QPoint()):
- """
- Simulate a drag/drop event from a source object to a specified target
- This has special case handling if the source is a QDockWidget (for docking) vs normal drag/drop
- source: The source object to initiate the drag from
- This is either a QWidget, or a tuple of (QAbstractItemView, QModelIndex) for dragging an item view item
- target: The target object to drop on after dragging
- This is either a QWidget, or a tuple of (QAbstractItemView, QModelIndex) for dropping on an item view item
- source_point: Optional, The QPoint to initiate the drag from. If none is specified, the center of the source will be used.
- target_point: Optional, The QPoint to drop on. If none is specified, the center of the target will be used.
- """
- # Flag if this drag/drop is for docking, which has some special cases
- docking = False
-
- # If the source is a tuple of (QAbstractItemView, QModelIndex), we need to use the
- # viewport() as the source, and find the location of the index
- if isinstance(source, tuple) and len(source) == 2:
- source_item_view = source[0]
- source_widget = source_item_view.viewport()
- source_model_index = source[1]
- source_rect = source_item_view.visualRect(source_model_index)
- else:
- # There are some special case actions if we are doing this drag for docking,
- # so figure this out by checking if the source is a QDockWidget
- if isinstance(source, QtWidgets.QDockWidget):
- docking = True
-
- source_widget = source
- source_rect = source.rect()
-
- # If the target is a tuple of (QAbstractItemView, QModelIndex), we need to use the
- # viewport() as the target, and find the location of the index
- if isinstance(target, tuple) and len(target) == 2:
- target_item_view = target[0]
- target_widget = target_item_view.viewport()
- target_model_index = target[1]
- target_rect = target_item_view.visualRect(target_model_index)
- else:
- # If we are doing a drag for docking, we actually want all the mouse events
- # to still be directed through the source widget
- if docking:
- target_widget = source_widget
- else:
- target_widget = target
- target_rect = target.rect()
-
- # If no source_point is specified, we need to find the center point of
- # the source widget
- if source_point.isNull():
- # If we are dragging for docking, initiate the drag from the center of the
- # dock widget title bar
- if docking:
- title_bar_widget = source.titleBarWidget()
- if title_bar_widget:
- source_point = title_bar_widget.geometry().center()
- else:
- raise RuntimeError("No titleBarWidget found for QDockWidget")
- # Otherwise, can just find the center of the rect
- else:
- source_point = source_rect.center()
-
- # If no target_point was specified, we need to find the center point of the target widget
- if target_point.isNull():
- target_point = target_rect.center()
-
- # If we are dragging for docking and we aren't dragging within the same source/target,
- # the mouse movements need to be directed to the source_widget, so we need to use the
- # difference in global positions of our source and target widgets to adjust the target_point
- # to be relative to the source
- if docking and source != target:
- source_top_left = source.mapToGlobal(QtCore.QPoint(0, 0))
- target_top_left = target.mapToGlobal(QtCore.QPoint(0, 0))
- offset = target_top_left - source_top_left
- target_point += offset
-
- # Move the mouse to the source spot where we will start the drag
- move_mouse(source_widget, source_point)
-
- # Press the left-mouse button to begin the drag
- QtTest.QTest.mousePress(source_widget, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, source_point)
-
- # If we are dragging for docking, we first need to drag the mouse past the minimum distance to
- # trigger the docking system properly
- if docking:
- drag_distance = QtWidgets.QApplication.startDragDistance() + 1
- docking_trigger_point = source_point + QtCore.QPoint(drag_distance, drag_distance)
- move_mouse(source_widget, docking_trigger_point)
-
- # Drag the mouse to the target widget over the desired point
- move_mouse(target_widget, target_point)
-
- # Release the left-mouse button to complete the drop.
- # If we are docking, we need to delay the actual mouse button release because the docking system has
- # a delay before the drop zone becomes active after it has been hovered, which can be found here:
- # FancyDockingDropZoneConstants::dockingTargetDelayMS = 110 ms
- # So we need to delay greater than dockingTargetDelayMS after the final mouse move
- # over the intended target.
- delay = -1
- if docking:
- delay = 200
- QtTest.QTest.mouseRelease(target_widget, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, target_point, delay)
-
- # Some drag/drop events have extra processing on the following event tick, so let those processEvents
- # first before we complete the drag/drop operation
- QtWidgets.QApplication.processEvents()
-
-
-def trigger_action_async(action):
- """
- Convenience function. Triggers an action asynchronously.
- This can be used if calling action.trigger might block (e.g. if it opens a modal dialog)
-
- action: The action to trigger
- """
- return run_soon(lambda: action.trigger())
-
-
-def click_button_async(button):
- """
- Convenience function. Clicks a button asynchronously.
- This can be used if calling button.click might block (e.g. if it opens a modal dialog)
-
- button: The button to click
- """
- return run_soon(lambda: button.click())
-
-
-async def wait_for_modal_widget(timeout=1.0):
- """
- Waits for an active modal widget and returns it.
- """
- return await wait_for(lambda: qApp.activeModalWidget(), timeout=timeout)
-
-async def wait_for_popup_widget(timeout=1.0):
- """
- Waits for an active popup widget and returns it.
- """
- return await wait_for(lambda: qApp.activePopupWidget(), timeout=timeout)
\ No newline at end of file
diff --git a/Tests/ly_shared/s3_utils.py b/Tests/ly_shared/s3_utils.py
deleted file mode 100755
index 5c516094f3..0000000000
--- a/Tests/ly_shared/s3_utils.py
+++ /dev/null
@@ -1,131 +0,0 @@
-"""
-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 pytest
-pytest.importorskip("boto3")
-import boto3
-import botocore.exceptions
-import logging
-import os
-
-import ly_test_tools.environment.file_system as file_system
-
-logger = logging.getLogger(__name__)
-
-
-class KeyExistsError(Exception):
- pass
-
-
-class KeyDoesNotExistError(Exception):
- pass
-
-
-class S3Utils(object):
- """
- Stores a boto3 S3 client to use for AWS S3 functionalities.
- """
- DEFAULT_REGION = 'us-west-2'
-
- def __init__(self, boto3_session=None):
- # type: (boto3.Session) -> None
- """
- The boto3 session can be set during init, or a default one will be created.
- :param boto3_session: A boto3 session
- """
- if boto3_session:
- self._session = boto3_session
- else:
- logger.info("No session provided, using default profile for s3 resource")
- self._session = boto3.session.Session()
- self._s3_resource = self._session.resource('s3')
-
- def upload_to_bucket(self, bucket_name, file_path, overwrite=False):
- """
- Uploads a given file to the given S3 bucket.
- :param bucket_name: Name of the S3 bucket where the file should be uploaded.
- :param file_path: Path to the target file.
- :param overwrite: Overwrite the key if it exists.
- """
- if not self.bucket_exists_in_s3(bucket_name):
- self._s3_resource.create_bucket(Bucket=bucket_name)
-
- s3_bucket = self._s3_resource.Bucket(bucket_name)
-
- file_key = os.path.basename(file_path)
- if not overwrite and self.key_exists_in_bucket(bucket_name, file_key):
- raise KeyExistsError("Key '{}' already exists in S3 bucket {}".format(file_key, bucket_name))
-
- s3_bucket.upload_file(file_path, file_key)
- logger.info("Uploading {} to S3 bucket {}".format(file_key, bucket_name))
-
- def download_from_bucket(self, bucket_name, file_key, destination_dir, file_name=None):
- """
- Download the given key from the given S3 bucket to the given destination. Logs an error if there is not enough \
- space available for the download.
- :param bucket_name: Name of the S3 bucket containing the desired file.
- :param file_key: Name of the file stored in S3.
- :param destination_dir: Directory where the file should be downloaded to.
- :param file_name: The name of the file you want to save it as. Defaults to the file_key.
- """
- self.bucket_exists_in_s3(bucket_name)
-
- if not self.key_exists_in_bucket(bucket_name, file_key):
- raise KeyDoesNotExistError("Key '{}' does not exist in S3 bucket {}".format(file_key, bucket_name))
-
- obj_summary = self._s3_resource.ObjectSummary(bucket_name, file_key)
- required_space = obj_summary.size
-
- file_system.check_free_space(destination_dir, required_space, "Insufficient space available for download:")
-
- if not os.path.exists(destination_dir):
- os.makedirs(destination_dir)
-
- if file_name is None:
- file_name = file_key
- destination_path = os.path.join(destination_dir, file_name)
- self._s3_resource.Object(bucket_name, file_key).download_file(destination_path)
- logger.info("Downloading {} to {}".format(file_key, destination_path))
-
- def bucket_exists_in_s3(self, bucket_name):
- """
- Verifies that the S3 bucket exists.
- :param bucket_name: Name of the S3 bucket that may or may not exist.
- :return: True if the bucket exists. False otherwise.
- """
- bucket_exists = True
-
- try:
- self._s3_resource.meta.client.head_bucket(Bucket=bucket_name)
- except botocore.exceptions.ClientError as err:
- if err.response['Error']['Code'] == '404':
- bucket_exists = False
-
- return bucket_exists
-
- def key_exists_in_bucket(self, bucket_name, file_key):
- """
- Verifies that the given key does not already exist in the given S3 bucket.
- :param bucket_name: Name of the S3 bucket that may or may not contain the file key.
- :param file_key: Name of the file key in question.
- :return: True if the key exists. False otherwise.
- """
- key_exists = True
- obj_summary = self._s3_resource.ObjectSummary(bucket_name, file_key)
-
- # Attempting to access any member of ObjectSummary for a nonexistent key will throw an exception
- # There is no built-in way to check key existence otherwise
- try:
- obj_summary.size
- except botocore.exceptions.ClientError as err:
- if err.response['Error']['Code'] == '404':
- key_exists = False
-
- return key_exists
diff --git a/Tests/ly_shared/screenshot_utils.py b/Tests/ly_shared/screenshot_utils.py
deleted file mode 100755
index c000cd8258..0000000000
--- a/Tests/ly_shared/screenshot_utils.py
+++ /dev/null
@@ -1,195 +0,0 @@
-"""
-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 string
-
-from .file_utils import move_file
-from . import phase as phase
-from ly_test_tools.environment.waiter import wait_for
-from ly_test_tools.image.screenshot_compare_qssim import qssim as compare_screenshots
-
-from ly_remote_console.remote_console_commands import capture_screenshot_command as capture_screenshot_command
-from ly_remote_console.remote_console_commands import send_command_and_expect_response as send_command_and_expect_response
-
-
-def get_next_screenshot_at_path(screenshot_path, prefix='screenshot', num_digits=4):
- """
- :param screenshot_path: Root folder where the screenshots are being generated by the Launcher pr Editor.
- :param prefix: Generated screenshot files are named sequentially using the prefix.
- e.g: screenshot0000.jpg, screenshot0001.jpg and so on.
- :param num_digits: How many digits are used for file name formation.
- :return: A string with the file name (relative to screenshot_path).
- """
- max_counter = 10**num_digits
- counter = 0
- while counter < max_counter:
- numberstr = "{}".format(counter)
- formattednumber = numberstr.zfill(num_digits)
- filename = "{}{}.jpg".format(prefix, formattednumber)
- filepath = os.path.join(screenshot_path, filename)
- if not os.path.exists(filepath):
- #This filename is available.
- return filename
- raise AssertionError("All possible screenshot names at directory {} are taken".format(screenshot_path))
-
-
-def take_screenshot(remote_console_instance, workspace, screenshot_name):
- """
- Takes an in game screenshot using the remote console instance passed in, validates that the screenshot exists
- and then renames that screenshot to something defined by the user of this function.
- :param remote_console_instance: Remote console instance that is attached to a specific launcher instance
- :param workspace: workspace instance so we can get the platform cache folder.
- :param screenshot_name: Name of the screenshot
- :return: None
- """
- screenshot_path = os.path.join(workspace.paths.platform_cache(), 'user', 'screenshots')
- expected_screenshot_name = get_next_screenshot_at_path(screenshot_path)
- capture_screenshot_command(remote_console_instance)
- wait_for(lambda: os.path.exists(os.path.join(screenshot_path, expected_screenshot_name)),
- timeout=10,
- exc=AssertionError('Screenshot at path:{} and with name:{} not found.'.format(screenshot_path, expected_screenshot_name)) )
- wait_for(lambda: rename_screenshot(screenshot_path, screenshot_name),
- timeout=10,
- exc=AssertionError('Screenshot at path:{} and with name:{} is still in use.'.format(screenshot_path, screenshot_name)))
-
-
-def rename_screenshot(screenshot_path, screenshot_name):
- """
- Tries to rename the screenshot when the file is done being written to
- :param screenshot_path: Path to the Screenshot folder
- :param screenshot_name: Name we wish to change the screenshot to
- :return: True when operation is completed, False if the file is still in use
- """
- try:
- src_img = os.path.join(screenshot_path, 'screenshot0000.jpg')
- dst_img = os.path.join(screenshot_path, '{}.jpg'.format(screenshot_name))
- print('Trying to rename {} to {}'.format(src_img, dst_img))
- os.rename(src_img, dst_img)
- return True
- except Exception as e:
- print('Found error {0} when trying to rename screenshot.'.format(str(e)))
- return False
-
-
-def move_screenshots(screenshot_path, file_type, logs_path):
- """
- Moves screenshots of a specific file type to the flume location so we can gather all of the screenshots we took.
- :param screenshot_path: Path to the screenshot folder
- :param file_type: Types of Files to look for. IE .jpg, .tif, etc
- :param logs_path: Path where flume gathers logs to be upload
- """
- for file_name in os.listdir(screenshot_path):
- if file_name.endswith(file_type):
- move_file(screenshot_path, logs_path, file_name)
-
-def move_screenshots_to_artifacts(screenshot_path, file_type, artifact_manager):
- """
- Saves screenshots of a specific file type to the artifact manager then removes the original files
- :param screenshot_path: Path to the screenshot folder
- :param file_type: Types of Files to look for. IE .jpg, .tif, etc
- :param artifact_manager: The artifact manager to save the artifacts to
- """
- for file_name in os.listdir(screenshot_path):
- if file_name.endswith(file_type):
- full_path_name = os.path.join(screenshot_path, file_name)
- artifact_manager.save_artifact(full_path_name)
- os.remove(full_path_name)
-
-
-
-def compare_golden_image(similarity_threshold, screenshot, screenshot_path, golden_image_name,
- golden_image_path=None):
- """
- This function assumes that your golden image filename contains the same base screenshot name and the word "golden"
- ex. pc_gamelobby_golden
-
- :param similarity_threshold: A float from 0.0 - 1.0 that determines how similar images must be or an asserts
- :param screenshot: A string that is the full name of the screenshot (ex. 'gamelobby_host.jpg')
- :param screenshot_path: A string that contains the path to the screenshots
- :param golden_image_path: A string that contains the path to the golden images, defaults to the screenshot_path
- :return:
- """
- if golden_image_path is None:
- golden_image_path = screenshot_path
-
- mean_similarity = compare_screenshots((os.path.join(screenshot_path, screenshot)),
- (os.path.join(golden_image_path, golden_image_name)))
- assert mean_similarity > similarity_threshold, \
- '{} screenshot comparison failed! Mean similarity value is: {}'\
- .format(screenshot, mean_similarity)
-
-def download_qa_golden_images(project_name, destination_dir, platform):
- """
- Downloads the golden images for a specified project from s3. The project_name, platform, and filetype are used to
- filter which images will be downloaded as the golden images.
-
- https://s3.console.aws.amazon.com/s3/buckets/ly-qae-jenkins-configs/golden-images/?region=us-west-1&tab=overview
-
- :param project_name: a string of the project name of the folder in s3. ex: 'MultiplayerSample'
- :param destination_dir: a string of where the images will be downloaded to
- :param platform: a string for the platform type ('pc', 'android', 'ios', 'darwin')
- :param filetype: a string for the file type. ex: '.jpg', '.png'
- :return:
- """
-
- # Currently we import s3_utils here instead of at the top because this is the only method that needs it,
- # and s3_utils has an unmet dependency on boto3 that hasn't been resolved. Once s3_utils is functional again,
- # this can move back to the top of the file.
- try:
- from . import s3_utils as s3_utils
- except ImportError:
- raise Exception("Failed to import s3_utils")
- # end s3_utils import
-
- bucket_name = 'ly-qae-jenkins-configs'
- path = 'golden-images/{}/{}/'.format(project_name, platform)
-
- if not s3_utils.key_exists_in_bucket(bucket_name, path):
- raise s3_utils.KeyDoesNotExistError("Key '{}' does not exist in S3 bucket {}".format(path, bucket_name))
- for image in s3_utils.s3.Bucket(bucket_name).objects.filter(Prefix=path):
- file_name = string.replace(image.key, path, '')
- if file_name != '':
- s3_utils.download_from_bucket(bucket_name, image.key, destination_dir, file_name)
-
-
-def _retry_command(remote_console_instance, command, output, tries=10, timeout=10):
- """
- Retries specified console command multiple times and asserts if it still can not send.
- :param remote_console: the remote console connected to the launcher.
- :param command: the command to send to the console.
- :param output: The expected output to check if the command was sent successfully.
- :param tries: The amount of times to try before asserting.
- :param timeout: The amount of time in seconds to wait for each retry send.
- :return: True if succeeded, will assert otherwise.
- """
- while tries > 0:
- tries -= 1
- try:
- send_command_and_expect_response(remote_console_instance, command, output)
- return True
- except:
- pass #Do nothing. Let the number of tries get to 0 if necessary.
- assert False, "Command \"{}\" failed to run in remote console.".format(command)
-
-
-def prepare_for_screenshot_compare(remote_console_instance):
- """
- Prepares launcher for screenshot comparison. Removes any debug text and antialiasing that may result in interference
- with the comparison.
-
- :param remote_console_instance: Remote console instance that is attached to a specific launcher instance
- :return:
- """
- wait_for(lambda: _retry_command(remote_console_instance, 'r_displayinfo 0',
- '$3r_DisplayInfo = $60 $5[DUMPTODISK, RESTRICTEDMODE]$4'))
- wait_for(lambda: _retry_command(remote_console_instance, 'r_antialiasingmode 0',
- '$3r_AntialiasingMode = $60 $5[]$4'))
diff --git a/Tests/performance/Scripts/apbatch_perf_summary.py b/Tests/performance/Scripts/apbatch_perf_summary.py
deleted file mode 100755
index ed34cbf490..0000000000
--- a/Tests/performance/Scripts/apbatch_perf_summary.py
+++ /dev/null
@@ -1,211 +0,0 @@
-"""
-
- 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.
-"""
-
-"""
-Script is designed to help with asset processor performance testing.
-
-This script is capable of running AssetProcessorBatch.exe and
-calculating how much time did it take to execute it. Also it is
-processing ap batch output and grabbing actual asset processing time.
-
-Apart from that script is capable of logging folder size
-(files, folders, actual size in bytes).
-
-Usage:
-python apbatch_perf_summary.py [-h] {folder_size,run_apbatch}
-
-python apbatch_perf_summary.py folder_size path project -cache
-
-Will show folder size: files, folders and actual size in bytes.
-
-build_path: Full path to the build dev directory, e.g. F:\builds\lumberyard-0.0-639162-pc-1985\dev
-project: Full project name (e.g. StarterGame or SamplesProject).
--cache: specify if you need to check cache folder instead of source assets.
-
-python apbatch_perf_summary.py run_apbatch build_path platform num_launches -delete_cache
-
-Will launch AssetProcessorBatch.exe num_launches times. Will return average
-running time and average asset processing time.
-
-build_path: Full path to the build dev directory, e.g. F:\builds\lumberyard-0.0-639162-pc-1985\dev
-platform: Which platform to launch - one of following: vc141, vc142, mac
-num_launches: How many times do you want to launch ap batch.
--delete_cache: specify if you want to delete Cache before each run
-"""
-
-
-import subprocess
-import time
-import os
-import argparse
-import test_tools.shared.file_system as fs
-import errno
-
-
-def run_ap_batch(build_path, platform):
- """
- Given a path to build will run ap batch and return total running and processing times.
- :param build_path: Full path to build dev, e.g. F:\builds\lumberyard-0.0-639162-pc-1985\dev
- :param platform: Specify platform where to run apbatch: vc141, vc142 or mac.
- :return: (processing_time, total_running_time) tuple.
- """
- assert os.path.exists(build_path)
-
- now = time.time()
- process = subprocess.Popen(['AssetProcessorBatch'], cwd=os.path.join(build_path, platform),
- shell=True, stdout=subprocess.PIPE)
- for line in iter(process.stdout.readline, ''):
- if 'Total Assets Processing Time' in line:
- processing_time = line.split(':')[2]
- process.wait()
- end = time.time()
-
- print 'Processing time: {}'.format(processing_time.strip())
- print 'Total time: {}s'.format(end - now)
-
- return float(processing_time.split('s')[0]), float(end - now)
-
-
-def run_several_times(build_path, platform, num_launches, erase_cache):
- """
- Given path to build, project name and boolean parameter (whether there is need to delete a cache)
- will run AssetProcessorBatch.exe num_launches and will return average running and processing times.
- :param build_path: Full path to build dev, e.g. F:\builds\lumberyard-0.0-639162-pc-1985\dev
- :param platform: Specify platform where to run apbatch: vc141, vc142 or mac.
- :param num_launches: How many times user needs to launch ap batch.
- :param erase_cache: yes/no or True/False in case user needs Cache folder to be deleted prior to apbatch launch.
- :return: (avg_processing_time, avg_running_time) tuple.
- """
- if not os.path.exists(build_path):
- raise IOError(errno.ENOENT, os.strerror(errno.ENOENT), build_path)
-
- average_processing_time = 0
- average_total_time = 0
-
- # running apbatch num_launches times and getting times
- for i in range(num_launches):
- if erase_cache and os.path.exists(os.path.join(build_path, 'Cache')):
- fs.delete([os.path.join(build_path, 'Cache')], False, True)
- print 'Iteration # {}'.format(i)
- processing_time, total_time = run_ap_batch(build_path, platform)
- average_processing_time += processing_time
- average_total_time += total_time
-
- # calculating average times
- average_processing_time /= num_launches
- average_total_time /= num_launches
-
- return average_processing_time, average_total_time
-
-
-def folder_size(path):
- """
- Given path to a build will calculate folder size.
- :param path: Full path to the folder for which you need folder size info.
- :return: (total_files_count, total_folders_count, total_size_in_bytes).
- """
- total_size = 0
- total_files_count = 0
- total_folder_count = 0
-
- if not os.path.exists(path):
- raise IOError(errno.ENOENT, os.strerror(errno.ENOENT), path)
-
- # walking over the folder and calculating files, folder; total files size
- for dirpath, dirnames, filenames in os.walk(path):
- total_folder_count += len(dirnames)
- total_files_count += len(filenames)
- for f in filenames:
- fp = os.path.join(dirpath, f)
- total_size += os.path.getsize(fp)
-
- return total_files_count, total_folder_count, total_size
-
-
-def run_apbatch(args):
- """
- Function for argparse command run_apbatch:
- running run_several_times function and printing its results.
- :param args: args.build_path: see run_several_times build_path.
- args.num_launches: see run_several_times num_launches.
- args.platform: see run_several_times platform.
- args.delete: see run_several_times erase_cache.
- :return: None
- """
- platform_bin = {
- 'vc141': 'Bin64vc141',
- 'vc142': 'Bin64vc142',
- 'mac': 'BinMac64'
- }
- running_times = run_several_times(args.build_path, platform_bin[args.platform], args.num_launches, args.delete_cache)
- print '\nAssets processing time: {}s'.format(running_times[0])
- print 'Total running time: {}s'.format(running_times[1])
-
-
-def print_folder_size(args):
- """
- Function for argparse command folder_size:
- running folder_size function and printing its results.
- :param args: args.build_path: see folder_size path.
- args.project: specified project which folder will be analyzed.
- args.cache: yes/no or True/False - whether user need to check Cache folder or not.
- :return: None
- """
- print '{} (cache: {}) folder size:'.format(args.project, args.cache)
- if args.cache:
- path = os.path.join(args.build_path, 'Cache', args.project)
- else:
- path = os.path.join(args.build_path, args.project)
- folder_size_data = folder_size(path)
- print 'Files: {}'.format(folder_size_data[0])
- print 'Folders: {}'.format(folder_size_data[1])
- print 'Size: {}'.format(folder_size_data[2])
-
-
-def main():
- """Main function with set-up and commands execution"""
- # creating command line arguments parser
- parser = argparse.ArgumentParser(prog = 'apbatch_perf_summary')
- subparsers = parser.add_subparsers(help = 'sub-command help', dest='command')
-
- parser_folder_size = subparsers.add_parser('folder_size',
- help='Will show folder size: files, folders and actual size in bytes. ')
- parser_run_apbatch = subparsers.add_parser('run_apbatch', help='run_apbatch help')
-
- parser_run_apbatch.add_argument('build_path',
- help='Full path to the build dev directory, e.g. '
- 'F:\\builds\\lumberyard-0.0-639162-pc-1985\\dev')
- parser_run_apbatch.add_argument('platform', choices=['vc141', 'vc142', 'mac'], help='vc141, vc142 or mac')
- parser_run_apbatch.add_argument('num_launches', type=int, help='How many times do you want to launch ap batch.')
- parser_run_apbatch.add_argument('-delete_cache', default=False, action='store_true',
- help='Specify if you want to delete Cache before and between runs')
-
- parser_run_apbatch.set_defaults(func=run_apbatch)
-
- parser_folder_size.add_argument('build_path',
- help='Full path to the build dev directory, e.g. '
- 'F:\\builds\\lumberyard-0.0-639162-pc-1985\dev')
- parser_folder_size.add_argument('project', help='Full project name (e.g. StarterGame or SamplesProject).')
- parser_folder_size.add_argument('-cache', default=False, action='store_true',
- help='Specify if you want to check cache folder', required=False)
- parser_folder_size.set_defaults(func=print_folder_size)
-
- args = parser.parse_args()
-
- # executing passed commands
- args.func(args)
-
-
-# calling main function if script is launched as standalone module
-if __name__ == '__main__':
- main()
-
diff --git a/Tests/pipeline/__init__.py b/Tests/pipeline/__init__.py
deleted file mode 100755
index e912252f4e..0000000000
--- a/Tests/pipeline/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""
-
- 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/Tests/pipeline/product_dependency_tests/AssetDependencyTests.py b/Tests/pipeline/product_dependency_tests/AssetDependencyTests.py
deleted file mode 100755
index 25e94b150c..0000000000
--- a/Tests/pipeline/product_dependency_tests/AssetDependencyTests.py
+++ /dev/null
@@ -1,88 +0,0 @@
-"""
-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.
-
-Automated scripts for tests calling AssetProcessorBatch validating basic features.
-
-"""
-
-from TestFixtures import HeliosProjectFixture
-
-import os
-import pytest
-import sqlite3
-import time
-import codecs
-
-def AssertProductHasDependencies(engineRoot, projectName, buildInfo, dbCheckWaitTime, product, pathDependencies, assetIdDependencies):
- productNameInDB = os.path.join(buildInfo.cacheSubfolder, projectName, product)
- productNameInDB = productNameInDB.replace("\\", "/")
-
- sqlDatabasePath = os.path.join(engineRoot, "Cache", projectName, "assetdb.sqlite")
- print (" * Connecting to database {}".format(sqlDatabasePath))
- sqlConnection = sqlite3.connect(sqlDatabasePath)
- productRowsList = list()
- productDbWait = dbCheckWaitTime
- while len(productRowsList) == 0 and productDbWait > 0:
- productRows = sqlConnection.execute(
- "SELECT ProductID FROM Products where ProductName='{}'".format(productNameInDB))
- productRowsList = list(productRows.fetchall())
- time.sleep(1)
- productDbWait = productDbWait - 1
- assert len(productRowsList) == 1, str.format("productRowsList= {}", productRowsList)
- productId = int(productRowsList[0][0])
-
- foundDependencies = list()
- dependencyDbTimeout = dbCheckWaitTime
- while len(foundDependencies) == 0 and dependencyDbTimeout > 0:
- dependencyRows = sqlConnection.execute(
- "SELECT * FROM ProductDependencies where ProductPK={}".format(productId))
- foundDependencies = list(dependencyRows.fetchall())
- time.sleep(1)
- dependencyDbTimeout = dependencyDbTimeout - 1
-
- foundAssetIds = list()
- foundUnresolvedPaths = list()
-
- uuidIndex = 2
- subIdIndex = 3
- unresolvedPathIndex = 6
- for foundDependency in foundDependencies:
- if foundDependency[unresolvedPathIndex] != "":
- # If there's a path, there won't be an asset ID
- foundUnresolvedPaths.append(foundDependency[unresolvedPathIndex])
- else:
- dependencyUUIDAsHex = codecs.encode(foundDependency[uuidIndex], 'hex_codec')
- subId = str(foundDependency[subIdIndex])
- assetId = "{}:{}".format(dependencyUUIDAsHex.decode('utf8'), subId)
- foundAssetIds.append(assetId)
-
- assert sorted(pathDependencies) == sorted(foundUnresolvedPaths)
- assert sorted(assetIdDependencies) == sorted(foundAssetIds)
-
-
-def test_VegdescriptorlistValidDependencies_DependenciesInDb(HeliosProjectFixture):
- engineRoot, projectName, buildInfo, dbCheckWaitTime = HeliosProjectFixture
- pathDependencies = {}
- assetIdDependencies = {
- # MeshAsset reference to "objects/default/primative_wedge_30.cgf"
- "e8b39f901f905e3998aa6f8ec4e91507:0",
- # MaterialAsset reference to "materials/am_grass1.mtl"
- "1151f14d38a65579888abe3139882e68:0"
- }
- AssertProductHasDependencies(engineRoot, projectName, buildInfo, dbCheckWaitTime, "heliosvegetation.vegdescriptorlist", pathDependencies, assetIdDependencies)
-
-def test_CloudLibrarytValidDependencies_DependenciesInDb(HeliosProjectFixture):
- engineRoot, projectName, buildInfo, dbCheckWaitTime = HeliosProjectFixture
- pathDependencies = {}
- assetIdDependencies = {
- # MaterialAsset reference to "materials/clouds/baseclouds.mtl"
- "f249f13854055cfba3b6d95bdc1a3db0:0"
- }
- AssertProductHasDependencies(engineRoot, projectName, buildInfo, dbCheckWaitTime, "libs/clouds/default.xml", pathDependencies, assetIdDependencies)
diff --git a/Tests/pipeline/product_dependency_tests/LvlDepTestDynamicSlice.py b/Tests/pipeline/product_dependency_tests/LvlDepTestDynamicSlice.py
deleted file mode 100755
index de7609c92c..0000000000
--- a/Tests/pipeline/product_dependency_tests/LvlDepTestDynamicSlice.py
+++ /dev/null
@@ -1,228 +0,0 @@
-"""
-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.
-
-Automated scripts for tests calling AssetProcessorBatch validating basic features.
-
-"""
-
-from TestFixtures import EmptyProjectFixture
-
-import fileinput
-import os
-import pytest
-import shutil
-import sqlite3
-import time
-
-import SubprocessUtils
-
-def MakeEditorPythonFile(testLevelName, assetGuid, tempFolder, templatePythonFile):
- outputFileName = templatePythonFile.replace(".template", ".py")
- outputFilePath = os.path.join(tempFolder, outputFileName)
- shutil.copy(os.path.join(os.path.dirname(os.path.realpath(
- __file__)), templatePythonFile), outputFilePath)
- for line in fileinput.FileInput(outputFilePath, inplace=1):
- line = line.replace("${LevelName}", str.format('"{}"', testLevelName))
- line = line.replace("${MeshGuid}", str.format('"{}"', assetGuid))
- print (line)
- return outputFilePath
-
-@pytest.mark.skip(reason="This test takes too long on Jenkins, and bundler tests catch everything we want from here")
-def test_productDependencies_EntityInLevelWithAssetReference_ReferencedAssetIsLevelProductDependency(
- EmptyProjectFixture, tmpdir):
- print ("RunLvlDynamicSliceTest")
- engineRoot, projectName, buildInfo, dbCheckWaitTime = EmptyProjectFixture
-
-
- tempFolder = tmpdir.mkdir("EditorPyScripts")
- print (" * Launch editor with dynamic slice test creation script")
-
- testLevelName = "SimpleLevel"
- expectedDependencyGuid = "81C4A6AF-C57D-5734-81B7-822074358C4D"
- exportLevelScriptPath = MakeEditorPythonFile(testLevelName, expectedDependencyGuid, str(tempFolder), "export_test_level.template")
-
- lyCommand = [buildInfo.editorExe, '/BatchMode', '/runpython', exportLevelScriptPath]
- SubprocessUtils.SubprocessWithTimeout(lyCommand, engineRoot, 60)
-
- # Logic that the export_test_level.py script will run in the editor:
- # * Create new level
- # * Place an entity in the level
- # * Add the mesh component to the entity
- # * Assign a test asset to that component [dev\Engine\Objects\default\primitive_sphere.cgf]
- # * Export the level
-
- print (" * Wait for the Asset Processor to copy the asset to the cache and update the asset database")
-
- projectCacheRoot = os.path.join(engineRoot, "Cache", projectName)
- levelRelativeSubFolder = os.path.join("Levels", testLevelName)
- cachePath = os.path.join(projectCacheRoot, buildInfo.cacheSubfolder, projectName, levelRelativeSubFolder, "level.pak")
- # On an i7 running Lumberyard on an SSD, it normally takes about 1-2 minutes to complete this step.
- # Add a few minutes onto that because the Jenkins machines may not be as fast.
- pakTimeoutSeconds = 10 * 60
- pakTimeoutWaitTimeSeconds = 1
- # Wait for the level.pak file to exist in the cache
- while not os.path.exists(cachePath) and pakTimeoutSeconds > 0:
- time.sleep(pakTimeoutWaitTimeSeconds)
- pakTimeoutSeconds -= pakTimeoutWaitTimeSeconds
-
- assert(os.path.exists(cachePath))
-
- print (" * Open the asset database, check that the correct product dependency is set for the exported level.pak")
-
- # A newly created level will have all of these dependencies by default.
- # These are tracked by the relative source path instead of the UUID because it's more readable.
- expectedDependencyPaths = {
- "materials/material_terrain_default.mtl", # from leveldata.xml
- "EngineAssets/Materials/sky/sky.mtl", # from mission_mission0.xml
- "EngineAssets/Materials/Water/ocean_default.mtl", # from mission_mission0.xml
- "textures/skys/night/half_moon.tif"
- }
-
- # Nothing is currently expected to unresolved, but this is left here in case that changes.
- expectedUnresolvedPaths = {
- # Hardcoded levelbuilder relative path output
- os.path.join(levelRelativeSubFolder, "auto_resourcelist.txt"),
- os.path.join(levelRelativeSubFolder, "level.cfg"),
- os.path.join(levelRelativeSubFolder, "levelparticles.xml"),
- os.path.join(levelRelativeSubFolder, "occluder.ocm"),
- os.path.join(levelRelativeSubFolder, "preloadlibs.txt"),
- os.path.join(levelRelativeSubFolder, "terrain", "cover.ctc"),
- os.path.join(levelRelativeSubFolder, "terrain", "merged_meshes_sectors", "mmrm_used_meshes.lst"),
- os.path.join(levelRelativeSubFolder, str.format("{}.xml",testLevelName))
- }
-
- # All expected GUIDs should match the format in the database: Lowercase, with no separators.
- expectedDependencyGuids = {
- expectedDependencyGuid.lower().replace('-','')
- }
-
- CheckDatabaseForDependency(projectCacheRoot, projectName, testLevelName, expectedDependencyGuids,
- expectedDependencyPaths, expectedUnresolvedPaths, buildInfo, dbCheckWaitTime)
-
- print ("/RunLvlDynamicSliceTest")
-
-
-def CheckDatabaseForDependency(projectCacheRoot, projectName, testLevelName, expectedDependencyGuids, expectedDependencyPaths, expectedUnresolvedPaths, buildInfo, dbCheckWaitTime):
- print ("CheckDatabaseForDependency")
-
- print (str.format(" * Checking expected dependencies for level.pak for level {}", testLevelName))
- print (str.format(" * Searching for these GUIDs as dependencies: {}", str(expectedDependencyGuids)))
- print (str.format(" * Searching for these paths as dependencies: {}", str(expectedDependencyPaths)))
- print (str.format(" * Searching for these paths as unresolved paths: {}", str(expectedUnresolvedPaths)))
-
- sqlDatabasePath = os.path.join(projectCacheRoot, "assetdb.sqlite")
- print (" * Connecting to database " + sqlDatabasePath)
- sqlConnection = sqlite3.connect(sqlDatabasePath)
- try:
- # Not using os.path.join because this is an expected string in a database
- levelPakProduct = str.format('{}/{}/levels/{}/level.pak', buildInfo.cacheSubfolder, projectName.lower(), testLevelName.lower())
- print (" * Looking in product table for " + levelPakProduct)
- productRows = sqlConnection.execute(
- str.format("SELECT ProductID FROM Products where ProductName='{}'", levelPakProduct))
-
- productRowsList = list(productRows.fetchall())
-
- productDbWait = dbCheckWaitTime
- while len(productRowsList) == 0 and productDbWait > 0:
- time.sleep(1)
- productDbWait = productDbWait - 1
- productRows = sqlConnection.execute(
- str.format("SELECT ProductID FROM Products where ProductName='{}'", levelPakProduct))
- productRowsList = list(productRows.fetchall())
-
-
- assert len(productRowsList) == 1, "productRowsList= {}".format(productRowsList)
-
- print (" * Searching product results for product ID")
- productId = int(productRowsList[0][0])
-
- assert productId
-
- print (str.format(" * Searching for dependencies for product ID {}", str(productId)))
- dependencyDbSuccess = False
- dependencyDbTimeout = dbCheckWaitTime
- # Make copies of the list in case multiple runs are required
- expectedUnresolvedPathsCopy = []
- expectedDependencyGuidsCopy = []
- remainingDependencies = []
-
- while (not dependencyDbSuccess) and dependencyDbTimeout > 0:
- expectedUnresolvedPathsCopy = expectedUnresolvedPaths.copy()
- expectedDependencyGuidsCopy = expectedDependencyGuids.copy()
- productDependencyRows = sqlConnection.execute("SELECT * FROM ProductDependencies where ProductPK={}".format(productId))
-
- dependencyRowIndex_SourceId = 2
- dependencyRowIndex_SubId = 3
- dependencyRowIndex_UnresolvedPath = 6
-
- productDependencyRowList = list(productDependencyRows.fetchall())
-
- expectedDependencyCount = len(expectedDependencyGuidsCopy) + len(
- expectedDependencyPaths) + len(expectedUnresolvedPathsCopy)
-
- dependencyDbSuccess = len(
- productDependencyRowList) == expectedDependencyCount
-
- expectedSubId = 0
-
- # This will contain SQL data buffers, which are not hashable.
- remainingDependencies = []
-
- for dependencyRow in productDependencyRowList:
- dependencySourceId = dependencyRow[dependencyRowIndex_SourceId]
- dependencySubId = int(dependencyRow[dependencyRowIndex_SubId])
- dependencyDbSuccess = dependencyDbSuccess and dependencySubId == expectedSubId
-
- dependencySourceAsHex = str(dependencySourceId).encode('hex')
- wasExpectedDependency = False
- # If this dependency's UUID is in our expected UUID list, then count it as found.
- if dependencySourceAsHex in expectedDependencyGuidsCopy:
- wasExpectedDependency = True
- expectedDependencyGuidsCopy.remove(dependencySourceAsHex)
-
- # If this dependency has an unresolved path that we expect, then count it as found.
- unresolvedPath = dependencyRow[dependencyRowIndex_UnresolvedPath]
- if unresolvedPath in expectedUnresolvedPathsCopy:
- wasExpectedDependency = True
- expectedUnresolvedPathsCopy.remove(unresolvedPath)
-
- if not wasExpectedDependency:
- remainingDependencies.append(dependencySourceId)
-
- dependencyDbSuccess = dependencyDbSuccess and (len(expectedDependencyGuidsCopy) == 0 and
- len(expectedUnresolvedPathsCopy) == 0 and
- len(remainingDependencies) == len(expectedDependencyPaths))
- if not dependencyDbSuccess:
- time.sleep(1)
- dependencyDbTimeout = dependencyDbTimeout - 1
-
- # do all the checks in asserts, instead of just assert dependencyDbSuccess so that error messages are more specific
- assert len(productDependencyRowList) == expectedDependencyCount, str.format("Expected {} dependencies, found {}", expectedDependencyCount, len(productDependencyRowList))
- assert len(expectedDependencyGuidsCopy) == 0, str.format(
- "Expected dependencies were not found in the asset database: {}", str(expectedDependencyGuids))
- assert len(expectedUnresolvedPathsCopy) == 0, str.format(
- "Expected unresolved paths were not found in the asset database: {}", str(expectedUnresolvedPathsCopy))
- assert len(remainingDependencies) == len(expectedDependencyPaths), str.format("Expected dependency sizes do not match for {} and {}", str(remainingDependencies), str(expectedDependencyPaths))
-
- for remainingDependency in remainingDependencies:
- sourceRows = sqlConnection.execute("SELECT SourceName FROM Sources where SourceGuid=?", (sqlite3.Binary(remainingDependency),) )
- sourceRowsList = list(sourceRows.fetchall())
- assert len(sourceRowsList) == 1, str.format("Expected to find 1 entry for {}, found {} instead.", str(remainingDependency).encode('hex'), len(sourceRowsList))
- sourcePath = sourceRowsList[0][0]
- assert sourcePath in expectedDependencyPaths, str.format("Could not find {} for UUID {} in the list of expected dependencies.", str(sourcePath), str(remainingDependency).encode('hex'))
- expectedDependencyPaths.remove(sourcePath)
- assert len(expectedDependencyPaths) == 0, str.format("Missing expected dependencies {}", str(expectedDependencyPaths))
-
- print (" * Found all expected dependencies")
- finally:
- print (" * Closing database connection")
- sqlConnection.close()
- print ("/CheckDatabaseForDependency")
-
diff --git a/Tests/pipeline/product_dependency_tests/SubprocessUtils.py b/Tests/pipeline/product_dependency_tests/SubprocessUtils.py
deleted file mode 100755
index d5637527f2..0000000000
--- a/Tests/pipeline/product_dependency_tests/SubprocessUtils.py
+++ /dev/null
@@ -1,52 +0,0 @@
-"""
-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.
-
-Automated scripts for tests calling AssetProcessorBatch validating basic features.
-
-"""
-
-import subprocess
-import threading
-import time
-
-class ThreadedSubprocess():
- def __init__(self, command, workingDirectory, timeOutMinutes):
- self.command = command
- self.workingDirectory = workingDirectory
- self.timeOutSeconds=timeOutMinutes*60
- self.process = None
- self.logOutput = []
- # Pytest doesn't handle asserts on other threads, capture them and report on the main thread.
- self.assertError = None
-
- def RunCommand(self):
- def RunThread():
- print (str.format("Subprocess thread starting for command: {}", self.command))
- self.process = subprocess.Popen(self.command, cwd=self.workingDirectory, shell=True, stdout=subprocess.PIPE, universal_newlines=True)
- for stdoutLine in iter(self.process.stdout.readline, ""):
- self.logOutput.append(stdoutLine)
- self.process.communicate()
- if self.process.returncode is None:
- self.assertError = str.format("Subprocess call '{}' had no return code", self.command)
- elif self.process.returncode != 0:
- self.assertError = str.format("Subprocess call '{}' returned code {}", self.command, self.process.returncode)
- print (str.format("Finished command, result {}: {}", self.process.returncode, self.command))
-
- commandThread = threading.Thread(target=RunThread)
- commandThread.start()
- commandThread.join(self.timeOutSeconds)
- assert not commandThread.is_alive(), str.format("Subprocess call '{}' timed out", self.command)
- assert self.assertError is None, self.assertError
-
-
-def SubprocessWithTimeout(command, workingDirectory, timeOutMinutes):
- threadedSubprocess = ThreadedSubprocess(command, workingDirectory, timeOutMinutes)
- threadedSubprocess.RunCommand()
- return threadedSubprocess.logOutput
diff --git a/Tests/pipeline/product_dependency_tests/TestAssets/updated_xml_schema_test.xmlschema b/Tests/pipeline/product_dependency_tests/TestAssets/updated_xml_schema_test.xmlschema
deleted file mode 100644
index a979a1b245..0000000000
--- a/Tests/pipeline/product_dependency_tests/TestAssets/updated_xml_schema_test.xmlschema
+++ /dev/null
@@ -1,53 +0,0 @@
-