Changed the .material file format to use a flat list for material property values, instead of a tree structure.

This is needed to support deeply nested material property groups, it just makes the serialization code a lot simpler than trying to support nested groups in the .material file. It also makes the file more readable and easier to search all files for particular properties.
I also updated MaterialSourceData to hide the property values behind a clean API.

Signed-off-by: santorac <55155825+santorac@users.noreply.github.com>
This commit is contained in:
santorac
2022-02-04 12:04:50 -08:00
parent 4bb93e4c94
commit 1daa9fbbed
11 changed files with 535 additions and 168 deletions
@@ -86,7 +86,7 @@ namespace UnitTest
RPITestFixture::TearDown();
}
Data::Asset<MaterialTypeAsset> CreateTestMaterialTypeAsset(Data::AssetId assetId)
{
const char* materialTypeJson = R"(
@@ -146,21 +146,21 @@ namespace UnitTest
}
)";
MaterialTypeSourceData materialTypeSourceData;
LoadTestDataFromJson(materialTypeSourceData, materialTypeJson);
return materialTypeSourceData.CreateMaterialTypeAsset(assetId).TakeValue();
}
};
void AddPropertyGroup(MaterialSourceData& material, AZStd::string_view groupName)
void AddPropertyGroup(MaterialSourceData&, AZStd::string_view)
{
material.m_properties.insert(groupName);
// Old function, left blank intentionally
}
void AddProperty(MaterialSourceData& material, AZStd::string_view groupName, AZStd::string_view propertyName, const MaterialPropertyValue& anyValue)
void AddProperty(MaterialSourceData& material, AZStd::string_view groupName, AZStd::string_view propertyName, const MaterialPropertyValue& value)
{
material.m_properties[groupName][propertyName] = anyValue;
MaterialPropertyId id{groupName, propertyName};
material.SetPropertyValue(id, value);
}
TEST_F(MaterialSourceDataTests, CreateMaterialAsset_BasicProperties)
@@ -359,80 +359,61 @@ namespace UnitTest
void CheckEqual(MaterialSourceData& a, MaterialSourceData& b)
{
EXPECT_STREQ(a.m_materialType.data(), b.m_materialType.data());
EXPECT_STREQ(a.m_description.data(), b.m_description.data());
EXPECT_STREQ(a.m_parentMaterial.data(), b.m_parentMaterial.data());
EXPECT_STREQ(a.m_materialType.c_str(), b.m_materialType.c_str());
EXPECT_STREQ(a.m_description.c_str(), b.m_description.c_str());
EXPECT_STREQ(a.m_parentMaterial.c_str(), b.m_parentMaterial.c_str());
EXPECT_EQ(a.m_materialTypeVersion, b.m_materialTypeVersion);
EXPECT_EQ(a.m_properties.size(), b.m_properties.size());
for (auto& groupA : a.m_properties)
{
AZStd::string groupName = groupA.first;
EXPECT_EQ(a.GetPropertyValues().size(), b.GetPropertyValues().size());
auto groupIterB = b.m_properties.find(groupName);
if (groupIterB == b.m_properties.end())
for (auto& [propertyId, propertyValue] : a.GetPropertyValues())
{
if (!b.HasPropertyValue(propertyId))
{
EXPECT_TRUE(false) << "groupB[" << groupName.c_str() << "] not found";
EXPECT_TRUE(false) << "Property '" << propertyId.GetCStr() << "' not found in material B";
continue;
}
auto& groupB = *groupIterB;
auto& propertyA = propertyValue;
auto& propertyB = b.GetPropertyValue(propertyId);
EXPECT_EQ(groupA.second.size(), groupB.second.size()) << " for group[" << groupName.c_str() << "]";
for (auto& propertyIterA : groupA.second)
{
AZStd::string propertyName = propertyIterA.first;
auto propertyIterB = groupB.second.find(propertyName);
if (propertyIterB == groupB.second.end())
{
EXPECT_TRUE(false) << "groupB[" << groupName.c_str() << "][" << propertyName.c_str() << "] not found";
continue;
}
auto& propertyA = propertyIterA.second;
auto& propertyB = propertyIterB->second;
AZStd::string propertyReference = AZStd::string::format(" for property '%s.%s'", groupName.c_str(), propertyName.c_str());
AZStd::string propertyReference = AZStd::string::format(" for property '%s'", propertyId.GetCStr());
// We allow some types like Vector4 and Color or Int and UInt to be interchangeable since they serialize the same and can be converted when the MaterialAsset is finalized.
// We allow some types like Vector4 and Color or Int and UInt to be interchangeable since they serialize the same and can be converted when the MaterialAsset is finalized.
if (AreTypesCompatible<bool>(propertyA, propertyB))
{
EXPECT_EQ(propertyA.GetValue<bool>(), propertyB.GetValue<bool>()) << propertyReference.c_str();
}
else if (AreTypesCompatible<int32_t>(propertyA, propertyB))
{
EXPECT_EQ(GetAsInt(propertyA), GetAsInt(propertyB)) << propertyReference.c_str();
}
else if (AreTypesCompatible<float>(propertyA, propertyB))
{
EXPECT_NEAR(propertyA.GetValue<float>(), propertyB.GetValue<float>(), 0.01) << propertyReference.c_str();
}
else if (AreTypesCompatible<Vector2>(propertyA, propertyB))
{
EXPECT_TRUE(propertyA.GetValue<Vector2>().IsClose(propertyB.GetValue<Vector2>())) << propertyReference.c_str();
}
else if (AreTypesCompatible<Vector3>(propertyA, propertyB))
{
EXPECT_TRUE(propertyA.GetValue<Vector3>().IsClose(propertyB.GetValue<Vector3>())) << propertyReference.c_str();
}
else if (AreTypesCompatible<Vector4>(propertyA, propertyB))
{
EXPECT_TRUE(GetAsVector4(propertyA).IsClose(GetAsVector4(propertyB))) << propertyReference.c_str();
}
else if (AreTypesCompatible<AZStd::string>(propertyA, propertyB))
{
EXPECT_STREQ(propertyA.GetValue<AZStd::string>().c_str(), propertyB.GetValue<AZStd::string>().c_str()) << propertyReference.c_str();
}
else
{
ADD_FAILURE();
}
if (AreTypesCompatible<bool>(propertyA, propertyB))
{
EXPECT_EQ(propertyA.GetValue<bool>(), propertyB.GetValue<bool>()) << propertyReference.c_str();
}
else if (AreTypesCompatible<int32_t>(propertyA, propertyB))
{
EXPECT_EQ(GetAsInt(propertyA), GetAsInt(propertyB)) << propertyReference.c_str();
}
else if (AreTypesCompatible<float>(propertyA, propertyB))
{
EXPECT_NEAR(propertyA.GetValue<float>(), propertyB.GetValue<float>(), 0.01) << propertyReference.c_str();
}
else if (AreTypesCompatible<Vector2>(propertyA, propertyB))
{
EXPECT_TRUE(propertyA.GetValue<Vector2>().IsClose(propertyB.GetValue<Vector2>())) << propertyReference.c_str();
}
else if (AreTypesCompatible<Vector3>(propertyA, propertyB))
{
EXPECT_TRUE(propertyA.GetValue<Vector3>().IsClose(propertyB.GetValue<Vector3>())) << propertyReference.c_str();
}
else if (AreTypesCompatible<Vector4>(propertyA, propertyB))
{
EXPECT_TRUE(GetAsVector4(propertyA).IsClose(GetAsVector4(propertyB))) << propertyReference.c_str();
}
else if (AreTypesCompatible<AZStd::string>(propertyA, propertyB))
{
EXPECT_STREQ(propertyA.GetValue<AZStd::string>().c_str(), propertyB.GetValue<AZStd::string>().c_str()) << propertyReference.c_str();
}
else
{
ADD_FAILURE();
}
}
}
TEST_F(MaterialSourceDataTests, TestJsonRoundTrip)
@@ -465,6 +446,88 @@ namespace UnitTest
CheckEqual(sourceDataOriginal, sourceDataCopy);
}
TEST_F(MaterialSourceDataTests, TestLoadLegacyFormat)
{
const AZStd::string inputJson = R"(
{
"materialType": "test.materialtype", // Doesn't matter, this isn't loaded
"properties": {
"groupA": {
"myBool": true,
"myInt": 5,
"myFloat": 0.5
},
"groupB": {
"myFloat2": [0.1, 0.2],
"myFloat3": [0.3, 0.4, 0.5],
"myFloat4": [0.6, 0.7, 0.8, 0.9],
"myString": "Hello"
}
}
}
)";
MaterialSourceData material;
LoadTestDataFromJson(material, inputJson);
material.ConvertToNewDataFormat();
MaterialSourceData expectedMaterial;
expectedMaterial.m_materialType = "test.materialtype";
expectedMaterial.SetPropertyValue(Name{"groupA.myBool"}, true);
expectedMaterial.SetPropertyValue(Name{"groupA.myInt"}, 5);
expectedMaterial.SetPropertyValue(Name{"groupA.myFloat"}, 0.5f);
expectedMaterial.SetPropertyValue(Name{"groupB.myFloat2"}, Vector2(0.1f, 0.2f));
expectedMaterial.SetPropertyValue(Name{"groupB.myFloat3"}, Vector3(0.3f, 0.4f, 0.5f));
expectedMaterial.SetPropertyValue(Name{"groupB.myFloat4"}, Vector4(0.6f, 0.7f, 0.8f, 0.9f));
expectedMaterial.SetPropertyValue(Name{"groupB.myString"}, AZStd::string{"Hello"});
CheckEqual(expectedMaterial, material);
}
TEST_F(MaterialSourceDataTests, TestPropertyValues)
{
MaterialSourceData material;
Name foo{"foo"};
Name bar{"bar"};
Name baz{"baz"};
EXPECT_EQ(0, material.GetPropertyValues().size());
EXPECT_FALSE(material.HasPropertyValue(foo));
EXPECT_FALSE(material.HasPropertyValue(bar));
EXPECT_FALSE(material.HasPropertyValue(baz));
EXPECT_FALSE(material.GetPropertyValue(foo).IsValid());
EXPECT_FALSE(material.GetPropertyValue(bar).IsValid());
EXPECT_FALSE(material.GetPropertyValue(baz).IsValid());
material.SetPropertyValue(Name{"foo"}, 2);
material.SetPropertyValue(Name{"bar"}, true);
material.SetPropertyValue(Name{"baz"}, 0.5f);
EXPECT_EQ(3, material.GetPropertyValues().size());
EXPECT_TRUE(material.HasPropertyValue(foo));
EXPECT_TRUE(material.HasPropertyValue(bar));
EXPECT_TRUE(material.HasPropertyValue(baz));
EXPECT_TRUE(material.GetPropertyValue(foo).IsValid());
EXPECT_TRUE(material.GetPropertyValue(bar).IsValid());
EXPECT_TRUE(material.GetPropertyValue(baz).IsValid());
EXPECT_EQ(material.GetPropertyValue(foo).GetValue<int32_t>(), 2);
EXPECT_EQ(material.GetPropertyValue(bar).GetValue<bool>(), true);
EXPECT_EQ(material.GetPropertyValue(baz).GetValue<float>(), 0.5f);
material.RemovePropertyValue(bar);
EXPECT_EQ(2, material.GetPropertyValues().size());
EXPECT_TRUE(material.HasPropertyValue(foo));
EXPECT_FALSE(material.HasPropertyValue(bar));
EXPECT_TRUE(material.HasPropertyValue(baz));
EXPECT_TRUE(material.GetPropertyValue(foo).IsValid());
EXPECT_FALSE(material.GetPropertyValue(bar).IsValid());
EXPECT_TRUE(material.GetPropertyValue(baz).IsValid());
EXPECT_EQ(material.GetPropertyValue(foo).GetValue<int32_t>(), 2);
EXPECT_EQ(material.GetPropertyValue(baz).GetValue<float>(), 0.5f);
}
TEST_F(MaterialSourceDataTests, Load_MaterialTypeAfterPropertyList)
{
@@ -487,21 +550,14 @@ namespace UnitTest
}
)";
const char* materialTypeFilePath = "@exefolder@/Temp/simpleMaterialType.materialtype";
AZ::IO::FileIOStream file;
EXPECT_TRUE(file.Open(materialTypeFilePath, AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath));
file.Write(simpleMaterialTypeJson.size(), simpleMaterialTypeJson.data());
file.Close();
AZ::Utils::WriteFile(simpleMaterialTypeJson, "@exefolder@/Temp/simpleMaterialType.materialtype");
// It shouldn't matter whether the materialType field appears before the property value list. This allows for the possibility
// that customer scripts generate material data and happen to use an unexpected order.
const AZStd::string inputJson = R"(
{
"properties": {
"general": {
"testValue": 1.2
}
"propertyValues": {
"general.testValue": 1.2
},
"materialType": "@exefolder@/Temp/simpleMaterialType.materialtype"
}
@@ -513,7 +569,7 @@ namespace UnitTest
EXPECT_EQ(AZ::JsonSerializationResult::Tasks::ReadField, loadResult.m_jsonResultCode.GetTask());
EXPECT_EQ(AZ::JsonSerializationResult::Processing::Completed, loadResult.m_jsonResultCode.GetProcessing());
float testValue = material.m_properties["general"]["testValue"].GetValue<float>();
float testValue = material.GetPropertyValue(Name{"general.testValue"}).GetValue<float>();
EXPECT_FLOAT_EQ(1.2f, testValue);
}
@@ -522,10 +578,8 @@ namespace UnitTest
const AZStd::string inputJson = R"(
{
"materialTypeVersion": 1,
"properties": {
"baseColor": {
"color": [1.0,1.0,1.0]
}
"propertyValues": {
"baseColor.color": [1.0,1.0,1.0]
}
}
)";
@@ -561,10 +615,8 @@ namespace UnitTest
{
"materialType": "DoesNotExist.materialtype",
"materialTypeVersion": 1,
"properties": {
"baseColor": {
"color": [1.0,1.0,1.0]
}
"propertyValues": {
"baseColor.color": [1.0,1.0,1.0]
}
}
)";
@@ -900,10 +952,8 @@ namespace UnitTest
const AZStd::string inputJson = AZStd::string::format(R"(
{
"materialType": "@exefolder@/Temp/test.materialtype",
"properties": {
"%s": {
"%s": %s
}
"propertyValues": {
"%s.%s": %s
}
}
)", groupName, propertyName, jsonValue);
@@ -965,7 +1015,239 @@ namespace UnitTest
CheckEndToEndDataTypeResolution("MyFloat4", "{\"y\":0.2, \"x\":0.1, \"Z\":0.3}", Vector4{0.1f, 0.2f, 0.3f, 0.0f});
CheckEndToEndDataTypeResolution("MyFloat4", "{\"y\":0.2, \"W\":0.4, \"x\":0.1, \"Z\":0.3}", Vector4{0.1f, 0.2f, 0.3f, 0.4f});
}
TEST_F(MaterialSourceDataTests, CreateMaterialAssetFromSourceData_MultiLevelDataInheritance)
{
// Note the data being tested here is based on CreateMaterialAsset_MultiLevelDataInheritance()
const AZStd::string simpleMaterialTypeJson = R"(
{
"propertyLayout": {
"propertyGroups":
[
{
"name": "general",
"properties": [
{
"name": "MyFloat",
"type": "Float"
},
{
"name": "MyFloat2",
"type": "Vector2"
},
{
"name": "MyColor",
"type": "Color"
}
]
}
]
}
}
)";
AZ::Utils::WriteFile(simpleMaterialTypeJson, "@exefolder@/Temp/test.materialtype");
const AZStd::string material1Json = R"(
{
"materialType": "@exefolder@/Temp/test.materialtype",
"propertyValues": {
"general.MyFloat": 1.5,
"general.MyColor": [0.1, 0.2, 0.3, 0.4]
}
}
)";
AZ::Utils::WriteFile(material1Json, "@exefolder@/Temp/m1.material");
const AZStd::string material2Json = R"(
{
"materialType": "@exefolder@/Temp/test.materialtype",
"parentMaterial": "@exefolder@/Temp/m1.material",
"propertyValues": {
"general.MyFloat2": [4.1, 4.2],
"general.MyColor": [0.15, 0.25, 0.35, 0.45]
}
}
)";
AZ::Utils::WriteFile(material2Json, "@exefolder@/Temp/m2.material");
const AZStd::string material3Json = R"(
{
"materialType": "@exefolder@/Temp/test.materialtype",
"parentMaterial": "@exefolder@/Temp/m2.material",
"propertyValues": {
"general.MyFloat": 3.5
}
}
)";
AZ::Utils::WriteFile(material3Json, "@exefolder@/Temp/m3.material");
MaterialSourceData sourceDataLevel1 = MaterialUtils::LoadMaterialSourceData("@exefolder@/Temp/m1.material").TakeValue();
MaterialSourceData sourceDataLevel2 = MaterialUtils::LoadMaterialSourceData("@exefolder@/Temp/m2.material").TakeValue();
MaterialSourceData sourceDataLevel3 = MaterialUtils::LoadMaterialSourceData("@exefolder@/Temp/m3.material").TakeValue();
auto materialAssetLevel1 = sourceDataLevel1.CreateMaterialAssetFromSourceData(Uuid::CreateRandom());
EXPECT_TRUE(materialAssetLevel1.IsSuccess());
EXPECT_TRUE(materialAssetLevel1.GetValue()->WasPreFinalized());
auto materialAssetLevel2 = sourceDataLevel2.CreateMaterialAssetFromSourceData(Uuid::CreateRandom());
EXPECT_TRUE(materialAssetLevel2.IsSuccess());
EXPECT_TRUE(materialAssetLevel2.GetValue()->WasPreFinalized());
auto materialAssetLevel3 = sourceDataLevel3.CreateMaterialAssetFromSourceData(Uuid::CreateRandom());
EXPECT_TRUE(materialAssetLevel3.IsSuccess());
EXPECT_TRUE(materialAssetLevel3.GetValue()->WasPreFinalized());
auto layout = materialAssetLevel1.GetValue()->GetMaterialPropertiesLayout();
MaterialPropertyIndex myFloat = layout->FindPropertyIndex(Name("general.MyFloat"));
MaterialPropertyIndex myFloat2 = layout->FindPropertyIndex(Name("general.MyFloat2"));
MaterialPropertyIndex myColor = layout->FindPropertyIndex(Name("general.MyColor"));
AZStd::span<const MaterialPropertyValue> properties;
// Check level 1 properties
properties = materialAssetLevel1.GetValue()->GetPropertyValues();
EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 1.5f);
EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(0.0f, 0.0f));
EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.1f, 0.2f, 0.3f, 0.4f));
// Check level 2 properties
properties = materialAssetLevel2.GetValue()->GetPropertyValues();
EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 1.5f);
EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(4.1f, 4.2f));
EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.15f, 0.25f, 0.35f, 0.45f));
// Check level 3 properties
properties = materialAssetLevel3.GetValue()->GetPropertyValues();
EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 3.5f);
EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(4.1f, 4.2f));
EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.15f, 0.25f, 0.35f, 0.45f));
}
TEST_F(MaterialSourceDataTests, CreateMaterialAssetFromSourceData_MultiLevelDataInheritance_OldFormat)
{
// This test is the same as CreateMaterialAssetFromSourceData_MultiLevelDataInheritance except it uses the old format
// where material property values in the .material file were nested, with properties listed under a group object,
// rather than using a flat list of property values.
// Basically, we are making sure that MaterialSourceData::ConvertToNewDataFormat() is getting called.
const AZStd::string simpleMaterialTypeJson = R"(
{
"propertyLayout": {
"propertyGroups":
[
{
"name": "general",
"properties": [
{
"name": "MyFloat",
"type": "Float"
},
{
"name": "MyFloat2",
"type": "Vector2"
},
{
"name": "MyColor",
"type": "Color"
}
]
}
]
}
}
)";
AZ::Utils::WriteFile(simpleMaterialTypeJson, "@exefolder@/Temp/test.materialtype");
const AZStd::string material1Json = R"(
{
"materialType": "@exefolder@/Temp/test.materialtype",
"properties": {
"general": {
"MyFloat": 1.5,
"MyColor": [0.1, 0.2, 0.3, 0.4]
}
}
}
)";
AZ::Utils::WriteFile(material1Json, "@exefolder@/Temp/m1.material");
const AZStd::string material2Json = R"(
{
"materialType": "@exefolder@/Temp/test.materialtype",
"parentMaterial": "@exefolder@/Temp/m1.material",
"properties": {
"general": {
"MyFloat2": [4.1, 4.2],
"MyColor": [0.15, 0.25, 0.35, 0.45]
}
}
}
)";
AZ::Utils::WriteFile(material2Json, "@exefolder@/Temp/m2.material");
const AZStd::string material3Json = R"(
{
"materialType": "@exefolder@/Temp/test.materialtype",
"parentMaterial": "@exefolder@/Temp/m2.material",
"properties": {
"general": {
"MyFloat": 3.5
}
}
}
)";
AZ::Utils::WriteFile(material3Json, "@exefolder@/Temp/m3.material");
MaterialSourceData sourceDataLevel1 = MaterialUtils::LoadMaterialSourceData("@exefolder@/Temp/m1.material").TakeValue();
MaterialSourceData sourceDataLevel2 = MaterialUtils::LoadMaterialSourceData("@exefolder@/Temp/m2.material").TakeValue();
MaterialSourceData sourceDataLevel3 = MaterialUtils::LoadMaterialSourceData("@exefolder@/Temp/m3.material").TakeValue();
auto materialAssetLevel1 = sourceDataLevel1.CreateMaterialAssetFromSourceData(Uuid::CreateRandom());
EXPECT_TRUE(materialAssetLevel1.IsSuccess());
EXPECT_TRUE(materialAssetLevel1.GetValue()->WasPreFinalized());
auto materialAssetLevel2 = sourceDataLevel2.CreateMaterialAssetFromSourceData(Uuid::CreateRandom());
EXPECT_TRUE(materialAssetLevel2.IsSuccess());
EXPECT_TRUE(materialAssetLevel2.GetValue()->WasPreFinalized());
auto materialAssetLevel3 = sourceDataLevel3.CreateMaterialAssetFromSourceData(Uuid::CreateRandom());
EXPECT_TRUE(materialAssetLevel3.IsSuccess());
EXPECT_TRUE(materialAssetLevel3.GetValue()->WasPreFinalized());
auto layout = materialAssetLevel1.GetValue()->GetMaterialPropertiesLayout();
MaterialPropertyIndex myFloat = layout->FindPropertyIndex(Name("general.MyFloat"));
MaterialPropertyIndex myFloat2 = layout->FindPropertyIndex(Name("general.MyFloat2"));
MaterialPropertyIndex myColor = layout->FindPropertyIndex(Name("general.MyColor"));
AZStd::span<const MaterialPropertyValue> properties;
// Check level 1 properties
properties = materialAssetLevel1.GetValue()->GetPropertyValues();
EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 1.5f);
EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(0.0f, 0.0f));
EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.1f, 0.2f, 0.3f, 0.4f));
// Check level 2 properties
properties = materialAssetLevel2.GetValue()->GetPropertyValues();
EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 1.5f);
EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(4.1f, 4.2f));
EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.15f, 0.25f, 0.35f, 0.45f));
// Check level 3 properties
properties = materialAssetLevel3.GetValue()->GetPropertyValues();
EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 3.5f);
EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(4.1f, 4.2f));
EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.15f, 0.25f, 0.35f, 0.45f));
}
}