From c8d2f74ca14f2b6bbc677aa25506ba33cb33a65a Mon Sep 17 00:00:00 2001 From: santorac <55155825+santorac@users.noreply.github.com> Date: Tue, 28 Sep 2021 22:05:26 -0700 Subject: [PATCH] Added backward-compatible support for the old "id" key in material type files, which is being renamed to "name". This required the use of custom serializers, because the JSON serialization system does not have any means of supporting field name aliases through SerializeContext. Testing: RPI unit test pass and AtomSampleViewer material screenshot test script passes. Signed-off-by: santorac <55155825+santorac@users.noreply.github.com> --- .../MaterialPropertyConnectionSerializer.h | 38 +++++ .../MaterialPropertyGroupSerializer.h | 38 +++++ .../Atom/RPI.Edit/Material/MaterialUtils.h | 18 +++ .../MaterialPropertyConnectionSerializer.cpp | 126 ++++++++++++++++ .../MaterialPropertyGroupSerializer.cpp | 125 +++++++++++++++ .../Material/MaterialPropertySerializer.cpp | 46 +++--- .../Material/MaterialTypeSourceData.cpp | 24 +-- .../RPI.Edit/Material/MaterialUtils.cpp | 25 +++ .../MaterialPropertySerializerTests.cpp | 35 +++++ .../Material/MaterialTypeSourceDataTests.cpp | 142 ++++++++++++++++++ Gems/Atom/RPI/Code/atom_rpi_edit_files.cmake | 4 + 11 files changed, 584 insertions(+), 37 deletions(-) create mode 100644 Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyConnectionSerializer.h create mode 100644 Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyGroupSerializer.h create mode 100644 Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyConnectionSerializer.cpp create mode 100644 Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyGroupSerializer.cpp diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyConnectionSerializer.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyConnectionSerializer.h new file mode 100644 index 0000000000..44b8805667 --- /dev/null +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyConnectionSerializer.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include + +namespace AZ +{ + class ReflectContext; + + namespace RPI + { + //! The property connection itself is rather simple, but we need this custom serializer to provide backward compatibility + //! for when the "id" key was changed to "name". If the JSON serialization system is ever updated to provide built-in + //! support for versioning, then we can probably remove this class. + class JsonMaterialPropertyConnectionSerializer + : public BaseJsonSerializer + { + public: + AZ_RTTI(JsonMaterialPropertyConnectionSerializer, "{2B7F00CF-51F7-4409-9C0E-914E59696FB9}", BaseJsonSerializer); + AZ_CLASS_ALLOCATOR_DECL; + + JsonSerializationResult::Result Load(void* outputValue, const Uuid& outputValueTypeId, const rapidjson::Value& inputValue, + JsonDeserializerContext& context) override; + + JsonSerializationResult::Result Store(rapidjson::Value& outputValue, const void* inputValue, + const void* defaultValue, const Uuid& valueTypeId, JsonSerializerContext& context) override; + }; + + } // namespace RPI +} // namespace AZ diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyGroupSerializer.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyGroupSerializer.h new file mode 100644 index 0000000000..138dace86a --- /dev/null +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyGroupSerializer.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include + +namespace AZ +{ + class ReflectContext; + + namespace RPI + { + //! The property group itself is rather simple, but we need this custom serializer to provide backward compatibility + //! for when the "id" key was changed to "name". If the JSON serialization system is ever updated to provide built-in + //! support for versioning, then we can probably remove this class. + class JsonMaterialPropertyGroupSerializer + : public BaseJsonSerializer + { + public: + AZ_RTTI(JsonMaterialPropertyGroupSerializer, "{74C56BBC-2084-46AF-9393-04C2FBDF6B20}", BaseJsonSerializer); + AZ_CLASS_ALLOCATOR_DECL; + + JsonSerializationResult::Result Load(void* outputValue, const Uuid& outputValueTypeId, const rapidjson::Value& inputValue, + JsonDeserializerContext& context) override; + + JsonSerializationResult::Result Store(rapidjson::Value& outputValue, const void* inputValue, + const void* defaultValue, const Uuid& valueTypeId, JsonSerializerContext& context) override; + }; + + } // namespace RPI +} // namespace AZ diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialUtils.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialUtils.h index 7801e0c3a7..d12e848a02 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialUtils.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialUtils.h @@ -15,6 +15,13 @@ namespace AZ { + class JsonDeserializerContext; + + namespace JsonSerializationResult + { + union ResultCode; + } + namespace RPI { class MaterialTypeSourceData; @@ -35,6 +42,17 @@ namespace AZ //! @param filePath a relative path if document is provided, an absolute path if document is not provided. //! @param document the loaded json document. AZ::Outcome LoadMaterialTypeSourceData(const AZStd::string& filePath, const rapidjson::Value* document = nullptr); + + //! Utility function for custom JSON serializers to report results as "Skipped" when encountering keys that aren't recognized + //! as part of the custom format. + //! @param acceptedFieldNames an array of names that are recognized by the custom format + //! @param acceptedFieldNameCount the number of elements in @acceptedFieldNames + //! @param object the JSON object being loaded + //! @param context the common JsonDeserializerContext that is central to the serialization process + //! @param result the ResultCode that well be updated with the Outcomes "Skipped" if an unrecognized field is encountered + void CheckForUnrecognizedJsonFields( + const AZStd::string_view* acceptedFieldNames, uint32_t acceptedFieldNameCount, + const rapidjson::Value& object, JsonDeserializerContext& context, JsonSerializationResult::ResultCode& result); } } } diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyConnectionSerializer.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyConnectionSerializer.cpp new file mode 100644 index 0000000000..a0be7b9dca --- /dev/null +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyConnectionSerializer.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +#include +#include +#include +#include + +namespace AZ +{ + namespace RPI + { + namespace JsonMaterialPropertyConnectionSerializerInternal + { + namespace Field + { + static constexpr const char type[] = "type"; + static constexpr const char name[] = "name"; + static constexpr const char id[] = "id"; // For backward compatibility + static constexpr const char shaderIndex[] = "shaderIndex"; + } + + static const AZStd::string_view AcceptedFields[] = + { + Field::type, + Field::name, + Field::id, + Field::shaderIndex + }; + } + + AZ_CLASS_ALLOCATOR_IMPL(JsonMaterialPropertyConnectionSerializer, SystemAllocator, 0); + + JsonSerializationResult::Result JsonMaterialPropertyConnectionSerializer::Load(void* outputValue, const Uuid& outputValueTypeId, + const rapidjson::Value& inputValue, JsonDeserializerContext& context) + { + namespace JSR = JsonSerializationResult; + using namespace JsonMaterialPropertyConnectionSerializerInternal; + + AZ_Assert(azrtti_typeid() == outputValueTypeId, + "Unable to deserialize material property connection to json because the provided type is %s", + outputValueTypeId.ToString().c_str()); + AZ_UNUSED(outputValueTypeId); + + MaterialTypeSourceData::PropertyConnection* propertyConnection = reinterpret_cast(outputValue); + AZ_Assert(propertyConnection, "Output value for JsonMaterialPropertyConnectionSerializer can't be null."); + + JSR::ResultCode result(JSR::Tasks::ReadField); + + if (!inputValue.IsObject()) + { + return context.Report(JsonSerializationResult::Tasks::ReadField, JsonSerializationResult::Outcomes::Unsupported, "Property connection must be a JSON object."); + } + + MaterialUtils::CheckForUnrecognizedJsonFields(AcceptedFields, AZ_ARRAY_SIZE(AcceptedFields), inputValue, context, result); + + result.Combine(ContinueLoadingFromJsonObjectField(&propertyConnection->m_type, azrtti_typeid(), inputValue, Field::type, context)); + + JsonSerializationResult::ResultCode nameResult = ContinueLoadingFromJsonObjectField(&propertyConnection->m_fieldName, azrtti_typeid(), inputValue, Field::name, context); + if (nameResult.GetOutcome() == JsonSerializationResult::Outcomes::DefaultsUsed) + { + // This "id" key is for backward compatibility. + result.Combine(ContinueLoadingFromJsonObjectField(&propertyConnection->m_fieldName, azrtti_typeid(), inputValue, Field::id, context)); + } + else + { + result.Combine(nameResult); + } + + result.Combine(ContinueLoadingFromJsonObjectField(&propertyConnection->m_shaderIndex, azrtti_typeid(), inputValue, Field::shaderIndex, context)); + + if (result.GetProcessing() == JsonSerializationResult::Processing::Completed) + { + return context.Report(result, "Successfully loaded property connection."); + } + else + { + return context.Report(result, "Partially loaded property connection."); + } + } + + + JsonSerializationResult::Result JsonMaterialPropertyConnectionSerializer::Store(rapidjson::Value& outputValue, const void* inputValue, + [[maybe_unused]] const void* defaultValue, const Uuid& valueTypeId, JsonSerializerContext& context) + { + namespace JSR = JsonSerializationResult; + using namespace JsonMaterialPropertyConnectionSerializerInternal; + + AZ_Assert(azrtti_typeid() == valueTypeId, + "Unable to serialize material property connection to json because the provided type is %s", + valueTypeId.ToString().c_str()); + AZ_UNUSED(valueTypeId); + + const MaterialTypeSourceData::PropertyConnection* propertyConnection = reinterpret_cast(inputValue); + AZ_Assert(propertyConnection, "Input value for JsonMaterialPropertyConnectionSerializer can't be null."); + + JSR::ResultCode result(JSR::Tasks::WriteValue); + + outputValue.SetObject(); + + MaterialTypeSourceData::PropertyConnection defaultConnection; + + result.Combine(ContinueStoringToJsonObjectField(outputValue, Field::type, &propertyConnection->m_type, &defaultConnection.m_type, azrtti_typeid(), context)); + result.Combine(ContinueStoringToJsonObjectField(outputValue, Field::name, &propertyConnection->m_fieldName, &defaultConnection.m_fieldName, azrtti_typeid(), context)); + result.Combine(ContinueStoringToJsonObjectField(outputValue, Field::shaderIndex, &propertyConnection->m_shaderIndex, &defaultConnection.m_shaderIndex, azrtti_typeid(), context)); + + if (result.GetProcessing() == JsonSerializationResult::Processing::Completed) + { + return context.Report(result, "Successfully stored property connection."); + } + else + { + return context.Report(result, "Partially stored property connection."); + } + } + + } // namespace RPI +} // namespace AZ diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyGroupSerializer.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyGroupSerializer.cpp new file mode 100644 index 0000000000..b54ba012d4 --- /dev/null +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyGroupSerializer.cpp @@ -0,0 +1,125 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +#include +#include +#include +#include + +namespace AZ +{ + namespace RPI + { + namespace JsonMaterialPropertyGroupSerializerInternal + { + namespace Field + { + static constexpr const char name[] = "name"; + static constexpr const char id[] = "id"; // For backward compatibility + static constexpr const char displayName[] = "displayName"; + static constexpr const char description[] = "description"; + } + + static const AZStd::string_view AcceptedFields[] = + { + Field::name, + Field::id, + Field::displayName, + Field::description + }; + } + + AZ_CLASS_ALLOCATOR_IMPL(JsonMaterialPropertyGroupSerializer, SystemAllocator, 0); + + JsonSerializationResult::Result JsonMaterialPropertyGroupSerializer::Load(void* outputValue, const Uuid& outputValueTypeId, + const rapidjson::Value& inputValue, JsonDeserializerContext& context) + { + namespace JSR = JsonSerializationResult; + using namespace JsonMaterialPropertyGroupSerializerInternal; + + AZ_Assert(azrtti_typeid() == outputValueTypeId, + "Unable to deserialize material property group to json because the provided type is %s", + outputValueTypeId.ToString().c_str()); + AZ_UNUSED(outputValueTypeId); + + MaterialTypeSourceData::GroupDefinition* propertyGroup = reinterpret_cast(outputValue); + AZ_Assert(propertyGroup, "Output value for JsonMaterialPropertyGroupSerializer can't be null."); + + JSR::ResultCode result(JSR::Tasks::ReadField); + + if (!inputValue.IsObject()) + { + return context.Report(JsonSerializationResult::Tasks::ReadField, JsonSerializationResult::Outcomes::Unsupported, "Property group must be a JSON object."); + } + + MaterialUtils::CheckForUnrecognizedJsonFields(AcceptedFields, AZ_ARRAY_SIZE(AcceptedFields), inputValue, context, result); + + JsonSerializationResult::ResultCode nameResult = ContinueLoadingFromJsonObjectField(&propertyGroup->m_name, azrtti_typeid(), inputValue, Field::name, context); + if (nameResult.GetOutcome() == JsonSerializationResult::Outcomes::DefaultsUsed) + { + // This "id" key is for backward compatibility. + result.Combine(ContinueLoadingFromJsonObjectField(&propertyGroup->m_name, azrtti_typeid(), inputValue, Field::id, context)); + } + else + { + result.Combine(nameResult); + } + + result.Combine(ContinueLoadingFromJsonObjectField(&propertyGroup->m_displayName, azrtti_typeid(), inputValue, Field::displayName, context)); + result.Combine(ContinueLoadingFromJsonObjectField(&propertyGroup->m_description, azrtti_typeid(), inputValue, Field::description, context)); + + if (result.GetProcessing() == JsonSerializationResult::Processing::Completed) + { + return context.Report(result, "Successfully loaded property group."); + } + else + { + return context.Report(result, "Partially loaded property group."); + } + } + + + JsonSerializationResult::Result JsonMaterialPropertyGroupSerializer::Store(rapidjson::Value& outputValue, const void* inputValue, + [[maybe_unused]] const void* defaultValue, const Uuid& valueTypeId, JsonSerializerContext& context) + { + namespace JSR = JsonSerializationResult; + using namespace JsonMaterialPropertyGroupSerializerInternal; + + AZ_Assert(azrtti_typeid() == valueTypeId, + "Unable to serialize material property group to json because the provided type is %s", + valueTypeId.ToString().c_str()); + AZ_UNUSED(valueTypeId); + + const MaterialTypeSourceData::GroupDefinition* propertyGroup = reinterpret_cast(inputValue); + AZ_Assert(propertyGroup, "Input value for JsonMaterialPropertyGroupSerializer can't be null."); + + JSR::ResultCode result(JSR::Tasks::WriteValue); + + outputValue.SetObject(); + + AZStd::string defaultEmpty; + + result.Combine(ContinueStoringToJsonObjectField(outputValue, Field::name, &propertyGroup->m_name, &defaultEmpty, azrtti_typeid(), context)); + result.Combine(ContinueStoringToJsonObjectField(outputValue, Field::displayName, &propertyGroup->m_displayName, &defaultEmpty, azrtti_typeid(), context)); + result.Combine(ContinueStoringToJsonObjectField(outputValue, Field::description, &propertyGroup->m_description, &defaultEmpty, azrtti_typeid(), context)); + + if (result.GetProcessing() == JsonSerializationResult::Processing::Completed) + { + return context.Report(result, "Successfully stored property group."); + } + else + { + return context.Report(result, "Partially stored property group."); + } + } + + } // namespace RPI +} // namespace AZ diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertySerializer.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertySerializer.cpp index b8b55a9fd5..4c66f753f2 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertySerializer.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertySerializer.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -23,11 +24,12 @@ namespace AZ { namespace RPI { - namespace // Avoid conflicts in uber builds + namespace JsonMaterialPropertySerializerInternal { namespace Field { static constexpr const char name[] = "name"; + static constexpr const char id[] = "id"; // For backward compatibility static constexpr const char displayName[] = "displayName"; static constexpr const char description[] = "description"; static constexpr const char type[] = "type"; @@ -47,6 +49,7 @@ namespace AZ static const AZStd::string_view AcceptedFields[] = { Field::name, + Field::id, Field::displayName, Field::description, Field::type, @@ -103,6 +106,7 @@ namespace AZ JsonDeserializerContext& context) { namespace JSR = JsonSerializationResult; + using namespace JsonMaterialPropertySerializerInternal; JSR::ResultCode result(JSR::Tasks::ReadField); @@ -160,6 +164,7 @@ namespace AZ JsonDeserializerContext& context) { namespace JSR = JsonSerializationResult; + using namespace JsonMaterialPropertySerializerInternal; JSR::ResultCode result(JSR::Tasks::ReadField); @@ -181,6 +186,7 @@ namespace AZ const rapidjson::Value& inputValue, JsonDeserializerContext& context) { namespace JSR = JsonSerializationResult; + using namespace JsonMaterialPropertySerializerInternal; AZ_Assert(azrtti_typeid() == outputValueTypeId, "Unable to deserialize material property to json because the provided type is %s", @@ -197,28 +203,19 @@ namespace AZ return context.Report(JsonSerializationResult::Tasks::ReadField, JsonSerializationResult::Outcomes::Unsupported, "Property definition must be a JSON object."); } - // First check for unexpected fields - for (auto iter = inputValue.MemberBegin(); iter != inputValue.MemberEnd(); ++iter) - { - bool matched = false; - - for (int i = 0; i < AZ_ARRAY_SIZE(AcceptedFields); ++i) - { - if (iter->name.GetString() == AcceptedFields[i]) - { - matched = true; - break; - } - } + MaterialUtils::CheckForUnrecognizedJsonFields(AcceptedFields, AZ_ARRAY_SIZE(AcceptedFields), inputValue, context, result); - if (!matched) - { - ScopedContextPath subPath{context, iter->name.GetString()}; - result.Combine(context.Report(JSR::Tasks::ReadField, JSR::Outcomes::Skipped, "Skipping unrecognized field")); - } + JsonSerializationResult::ResultCode nameResult = ContinueLoadingFromJsonObjectField(&property->m_name, azrtti_typeid(), inputValue, Field::name, context); + if (nameResult.GetOutcome() == JsonSerializationResult::Outcomes::DefaultsUsed) + { + // This "id" key is for backward compatibility. + result.Combine(ContinueLoadingFromJsonObjectField(&property->m_name, azrtti_typeid(), inputValue, Field::id, context)); + } + else + { + result.Combine(nameResult); } - result.Combine(ContinueLoadingFromJsonObjectField(&property->m_name, azrtti_typeid(), inputValue, Field::name, context)); result.Combine(ContinueLoadingFromJsonObjectField(&property->m_displayName, azrtti_typeid(), inputValue, Field::displayName, context)); result.Combine(ContinueLoadingFromJsonObjectField(&property->m_description, azrtti_typeid(), inputValue, Field::description, context)); result.Combine(ContinueLoadingFromJsonObjectField(&property->m_dataType, azrtti_typeid(), inputValue, Field::type, context)); @@ -302,6 +299,8 @@ namespace AZ JsonSerializerContext& context) { namespace JSR = JsonSerializationResult; + using namespace JsonMaterialPropertySerializerInternal; + JSR::ResultCode result(JSR::Tasks::WriteValue); if (property->m_value.Is()) @@ -345,6 +344,8 @@ namespace AZ JsonSerializerContext& context) { namespace JSR = JsonSerializationResult; + using namespace JsonMaterialPropertySerializerInternal; + JsonSerializationResult::ResultCode result(JSR::Tasks::WriteValue); if (property->m_value.Is()) @@ -360,6 +361,7 @@ namespace AZ [[maybe_unused]] const void* defaultValue, const Uuid& valueTypeId, JsonSerializerContext& context) { namespace JSR = JsonSerializationResult; + using namespace JsonMaterialPropertySerializerInternal; AZ_Assert(azrtti_typeid() == valueTypeId, "Unable to serialize material property to json because the provided type is %s", @@ -452,6 +454,8 @@ namespace AZ const rapidjson::Value& inputValue, JsonDeserializerContext& context) { namespace JSR = JsonSerializationResult; + using namespace JsonMaterialPropertySerializerInternal; + JSR::ResultCode result(JSR::Tasks::ReadField); if (inputValue.HasMember(Field::vectorLabels)) @@ -467,6 +471,8 @@ namespace AZ { AZStd::string emptyString; namespace JSR = JsonSerializationResult; + using namespace JsonMaterialPropertySerializerInternal; + JsonSerializationResult::ResultCode result(JSR::Tasks::WriteValue); if (!property->m_vectorLabels.empty()) 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 a13a8df16e..1cc57f4d47 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include @@ -46,29 +48,17 @@ namespace AZ if (JsonRegistrationContext* jsonContext = azrtti_cast(context)) { jsonContext->Serializer()->HandlesType(); + jsonContext->Serializer()->HandlesType(); + jsonContext->Serializer()->HandlesType(); } else if (auto* serializeContext = azrtti_cast(context)) { - serializeContext->Class() - ->Version(2) - ->Field("type", &PropertyConnection::m_type) - ->Field("name", &PropertyConnection::m_fieldName) - ->Field("shaderIndex", &PropertyConnection::m_shaderIndex) - ; + serializeContext->Class()->Version(3); + serializeContext->Class()->Version(4); + serializeContext->Class()->Version(1); serializeContext->RegisterGenericType(); - serializeContext->Class() - ->Version(2) - ->Field("name", &GroupDefinition::m_name) - ->Field("displayName", &GroupDefinition::m_displayName) - ->Field("description", &GroupDefinition::m_description) - ; - - serializeContext->Class() - ->Version(1) - ; - serializeContext->Class() ->Version(2) ->Field("file", &ShaderVariantReferenceData::m_shaderFilePath) diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialUtils.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialUtils.cpp index 62f4f02e3c..90ce9e66ce 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialUtils.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialUtils.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include @@ -99,6 +101,29 @@ namespace AZ return AZ::Success(AZStd::move(materialType)); } } + + void CheckForUnrecognizedJsonFields(const AZStd::string_view* acceptedFieldNames, uint32_t acceptedFieldNameCount, const rapidjson::Value& object, JsonDeserializerContext& context, JsonSerializationResult::ResultCode &result) + { + for (auto iter = object.MemberBegin(); iter != object.MemberEnd(); ++iter) + { + bool matched = false; + + for (uint32_t i = 0; i < acceptedFieldNameCount; ++i) + { + if (iter->name.GetString() == acceptedFieldNames[i]) + { + matched = true; + break; + } + } + + if (!matched) + { + ScopedContextPath subPath{context, iter->name.GetString()}; + result.Combine(context.Report(JsonSerializationResult::Tasks::ReadField, JsonSerializationResult::Outcomes::Skipped, "Skipping unrecognized field")); + } + } + } } } } diff --git a/Gems/Atom/RPI/Code/Tests/Material/MaterialPropertySerializerTests.cpp b/Gems/Atom/RPI/Code/Tests/Material/MaterialPropertySerializerTests.cpp index 5705c3416a..9afc05a237 100644 --- a/Gems/Atom/RPI/Code/Tests/Material/MaterialPropertySerializerTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Material/MaterialPropertySerializerTests.cpp @@ -828,6 +828,41 @@ namespace UnitTest TestStoreToJson(propertyData, inputJson); } + + TEST_F(MaterialPropertySerializerTests, LoadUsingOldFormat) + { + // Tests backward compatibility for when "id" was the key instead of "name", for both the property and its connections. + + const AZStd::string inputJson = R"( + { + "id": "testProperty", + "type": "Float", + "connection": { + "type": "ShaderOption", + "id": "o_foo", + "shaderIndex": 2 + } + } + )"; + + MaterialTypeSourceData::PropertyDefinition propertyData; + JsonTestResult loadResult = LoadTestDataFromJson(propertyData, inputJson); + + EXPECT_EQ(AZ::JsonSerializationResult::Tasks::ReadField, loadResult.m_jsonResultCode.GetTask()); + EXPECT_EQ(AZ::JsonSerializationResult::Processing::Completed, loadResult.m_jsonResultCode.GetProcessing()); + + EXPECT_EQ("testProperty", propertyData.m_name); + + EXPECT_EQ(1, propertyData.m_outputConnections.size()); + EXPECT_EQ(MaterialPropertyOutputType::ShaderOption, propertyData.m_outputConnections[0].m_type); + EXPECT_EQ("o_foo", propertyData.m_outputConnections[0].m_fieldName); + EXPECT_EQ(2, propertyData.m_outputConnections[0].m_shaderIndex); + + EXPECT_TRUE(loadResult.ContainsMessage("/connection/type", "Success")); + EXPECT_TRUE(loadResult.ContainsMessage("/connection/id", "Success")); + EXPECT_TRUE(loadResult.ContainsMessage("/connection/shaderIndex", "Success")); + EXPECT_FALSE(loadResult.ContainsOutcome(JsonSerializationResult::Outcomes::Skipped)); + } TEST_F(MaterialPropertySerializerTests, LoadAndStoreJson_MultipleConnections) { diff --git a/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp b/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp index c37943f79c..ba4a58b9ff 100644 --- a/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp @@ -1115,6 +1115,148 @@ namespace UnitTest JsonTestResult storeResult = StoreTestDataToJson(material, outputJson); ExpectSimilarJson(inputJson, outputJson); } + + TEST_F(MaterialTypeSourceDataTests, LoadAllFieldsUsingOldFormat) + { + // The content of this test was copied from LoadAndStoreJson_AllFields to prove backward compatibility. + // (The "store" part of the test was not included because the saved data will be the new format). + + const AZStd::string inputJson = R"( + { + "description": "This is a general description about the material", + "propertyLayout": { + "version": 2, + "groups": [ + { + "id": "groupA", + "displayName": "Property Group A", + "description": "Description of property group A" + }, + { + "id": "groupB", + "displayName": "Property Group B", + "description": "Description of property group B" + } + ], + "properties": { + "groupA": [ + { + "id": "foo", + "type": "Bool", + "defaultValue": true + }, + { + "id": "bar", + "type": "Image", + "defaultValue": "Default.png", + "visibility": "Hidden" + } + ], + "groupB": [ + { + "id": "foo", + "type": "Float", + "defaultValue": 0.5 + }, + { + "id": "bar", + "type": "Color", + "defaultValue": [0.5, 0.5, 0.5], + "visibility": "Disabled" + } + ] + } + }, + "shaders": [ + { + "file": "ForwardPass.shader", + "tag": "ForwardPass", + "options": { + "o_optionA": "False", + "o_optionB": "True" + } + }, + { + "file": "DepthPass.shader", + "options": { + "o_optionC": "1", + "o_optionD": "2" + } + } + ], + "functors": [ + { + "type": "EnableShader", + "args": { + "enablePassProperty": "groupA.foo", + "shaderIndex": 1 + } + }, + { + "type": "Splat3", + "args": { + "floatPropertyInput": "groupB.foo", + "float3ShaderSettingOutput": "m_someFloat3" + } + } + ] + } + )"; + + MaterialTypeSourceData material; + JsonTestResult loadResult = LoadTestDataFromJson(material, inputJson); + + EXPECT_EQ(material.m_description, "This is a general description about the material"); + + EXPECT_EQ(material.m_propertyLayout.m_version, 2); + + EXPECT_EQ(material.m_propertyLayout.m_groups.size(), 2); + EXPECT_TRUE(material.FindGroup("groupA") != nullptr); + EXPECT_TRUE(material.FindGroup("groupB") != nullptr); + EXPECT_EQ(material.FindGroup("groupA")->m_displayName, "Property Group A"); + EXPECT_EQ(material.FindGroup("groupB")->m_displayName, "Property Group B"); + EXPECT_EQ(material.FindGroup("groupA")->m_description, "Description of property group A"); + EXPECT_EQ(material.FindGroup("groupB")->m_description, "Description of property group B"); + + EXPECT_EQ(material.m_propertyLayout.m_properties.size(), 2); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupA"].size(), 2); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupB"].size(), 2); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupA"][0].m_name, "foo"); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupA"][1].m_name, "bar"); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupB"][0].m_name, "foo"); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupB"][1].m_name, "bar"); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupA"][0].m_dataType, MaterialPropertyDataType::Bool); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupA"][1].m_dataType, MaterialPropertyDataType::Image); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupB"][0].m_dataType, MaterialPropertyDataType::Float); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupB"][1].m_dataType, MaterialPropertyDataType::Color); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupA"][0].m_visibility, MaterialPropertyVisibility::Enabled); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupA"][1].m_visibility, MaterialPropertyVisibility::Hidden); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupB"][0].m_visibility, MaterialPropertyVisibility::Enabled); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupB"][1].m_visibility, MaterialPropertyVisibility::Disabled); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupA"][0].m_value, true); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupA"][1].m_value, AZStd::string{"Default.png"}); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupB"][0].m_value, 0.5f); + EXPECT_EQ(material.m_propertyLayout.m_properties["groupB"][1].m_value, AZ::Color(0.5f, 0.5f, 0.5f, 1.0f)); + + EXPECT_EQ(material.m_shaderCollection.size(), 2); + EXPECT_EQ(material.m_shaderCollection[0].m_shaderFilePath, "ForwardPass.shader"); + EXPECT_EQ(material.m_shaderCollection[1].m_shaderFilePath, "DepthPass.shader"); + EXPECT_EQ(material.m_shaderCollection[0].m_shaderOptionValues.size(), 2); + EXPECT_EQ(material.m_shaderCollection[1].m_shaderOptionValues.size(), 2); + EXPECT_EQ(material.m_shaderCollection[0].m_shaderOptionValues[Name{"o_optionA"}], Name{"False"}); + EXPECT_EQ(material.m_shaderCollection[0].m_shaderOptionValues[Name{"o_optionB"}], Name{"True"}); + EXPECT_EQ(material.m_shaderCollection[1].m_shaderOptionValues[Name{"o_optionC"}], Name{"1"}); + EXPECT_EQ(material.m_shaderCollection[1].m_shaderOptionValues[Name{"o_optionD"}], Name{"2"}); + EXPECT_EQ(material.m_shaderCollection[0].m_shaderTag, Name{"ForwardPass"}); + + EXPECT_EQ(material.m_materialFunctorSourceData.size(), 2); + EXPECT_TRUE(azrtti_cast(material.m_materialFunctorSourceData[0]->GetActualSourceData().get())); + EXPECT_EQ(azrtti_cast(material.m_materialFunctorSourceData[0]->GetActualSourceData().get())->m_enablePassPropertyId, "groupA.foo"); + EXPECT_EQ(azrtti_cast(material.m_materialFunctorSourceData[0]->GetActualSourceData().get())->m_shaderIndex, 1); + EXPECT_TRUE(azrtti_cast(material.m_materialFunctorSourceData[1]->GetActualSourceData().get())); + EXPECT_EQ(azrtti_cast(material.m_materialFunctorSourceData[1]->GetActualSourceData().get())->m_floatPropertyInputId, "groupB.foo"); + EXPECT_EQ(azrtti_cast(material.m_materialFunctorSourceData[1]->GetActualSourceData().get())->m_float3ShaderSettingOutputId, "m_someFloat3"); + } TEST_F(MaterialTypeSourceDataTests, CreateMaterialTypeAsset_PropertyImagePath) { diff --git a/Gems/Atom/RPI/Code/atom_rpi_edit_files.cmake b/Gems/Atom/RPI/Code/atom_rpi_edit_files.cmake index c7a459f217..e32d19ffd7 100644 --- a/Gems/Atom/RPI/Code/atom_rpi_edit_files.cmake +++ b/Gems/Atom/RPI/Code/atom_rpi_edit_files.cmake @@ -18,6 +18,8 @@ set(FILES Include/Atom/RPI.Edit/Material/MaterialTypeSourceData.h Include/Atom/RPI.Edit/Material/MaterialConverterBus.h Include/Atom/RPI.Edit/Material/MaterialPropertyId.h + Include/Atom/RPI.Edit/Material/MaterialPropertyConnectionSerializer.h + Include/Atom/RPI.Edit/Material/MaterialPropertyGroupSerializer.h Include/Atom/RPI.Edit/Material/MaterialPropertySerializer.h Include/Atom/RPI.Edit/Material/MaterialPropertyValueSerializer.h Include/Atom/RPI.Edit/Material/MaterialPropertyValueSourceData.h @@ -36,6 +38,8 @@ set(FILES Source/RPI.Edit/Material/LuaMaterialFunctorSourceData.cpp Source/RPI.Edit/Material/MaterialTypeSourceData.cpp Source/RPI.Edit/Material/MaterialPropertyId.cpp + Source/RPI.Edit/Material/MaterialPropertyGroupSerializer.cpp + Source/RPI.Edit/Material/MaterialPropertyConnectionSerializer.cpp Source/RPI.Edit/Material/MaterialPropertySerializer.cpp Source/RPI.Edit/Material/MaterialPropertyValueSerializer.cpp Source/RPI.Edit/Material/MaterialPropertyValueSourceData.cpp