Formalize material schema and constant layout contract
This commit is contained in:
@@ -66,6 +66,66 @@ void WriteTextFile(const std::filesystem::path& path, const std::string& content
|
||||
ASSERT_TRUE(static_cast<bool>(output));
|
||||
}
|
||||
|
||||
std::filesystem::path WriteSchemaMaterialShaderManifest(const std::filesystem::path& rootPath) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path shaderDir = rootPath / "Shaders";
|
||||
fs::create_directories(shaderDir);
|
||||
|
||||
WriteTextFile(shaderDir / "schema.vert.glsl", "#version 430\nvoid main() {}\n");
|
||||
WriteTextFile(shaderDir / "schema.frag.glsl", "#version 430\nvoid main() {}\n");
|
||||
|
||||
const fs::path manifestPath = shaderDir / "schema.shader";
|
||||
std::ofstream manifest(manifestPath, std::ios::binary | std::ios::trunc);
|
||||
EXPECT_TRUE(manifest.is_open());
|
||||
if (!manifest.is_open()) {
|
||||
return {};
|
||||
}
|
||||
manifest << "{\n";
|
||||
manifest << " \"name\": \"SchemaMaterialShader\",\n";
|
||||
manifest << " \"properties\": [\n";
|
||||
manifest << " {\n";
|
||||
manifest << " \"name\": \"_BaseColor\",\n";
|
||||
manifest << " \"displayName\": \"Base Color\",\n";
|
||||
manifest << " \"type\": \"Color\",\n";
|
||||
manifest << " \"defaultValue\": \"(1,0.5,0.25,1)\",\n";
|
||||
manifest << " \"semantic\": \"BaseColor\"\n";
|
||||
manifest << " },\n";
|
||||
manifest << " {\n";
|
||||
manifest << " \"name\": \"_Metallic\",\n";
|
||||
manifest << " \"displayName\": \"Metallic\",\n";
|
||||
manifest << " \"type\": \"Float\",\n";
|
||||
manifest << " \"defaultValue\": \"0.7\"\n";
|
||||
manifest << " },\n";
|
||||
manifest << " {\n";
|
||||
manifest << " \"name\": \"_Mode\",\n";
|
||||
manifest << " \"displayName\": \"Mode\",\n";
|
||||
manifest << " \"type\": \"Int\",\n";
|
||||
manifest << " \"defaultValue\": \"2\"\n";
|
||||
manifest << " },\n";
|
||||
manifest << " {\n";
|
||||
manifest << " \"name\": \"_MainTex\",\n";
|
||||
manifest << " \"displayName\": \"Main Tex\",\n";
|
||||
manifest << " \"type\": \"Texture2D\",\n";
|
||||
manifest << " \"defaultValue\": \"white\",\n";
|
||||
manifest << " \"semantic\": \"BaseColorTexture\"\n";
|
||||
manifest << " }\n";
|
||||
manifest << " ],\n";
|
||||
manifest << " \"passes\": [\n";
|
||||
manifest << " {\n";
|
||||
manifest << " \"name\": \"ForwardLit\",\n";
|
||||
manifest << " \"variants\": [\n";
|
||||
manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"schema.vert.glsl\" },\n";
|
||||
manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"schema.frag.glsl\" }\n";
|
||||
manifest << " ]\n";
|
||||
manifest << " }\n";
|
||||
manifest << " ]\n";
|
||||
manifest << "}\n";
|
||||
EXPECT_TRUE(static_cast<bool>(manifest));
|
||||
|
||||
return manifestPath;
|
||||
}
|
||||
|
||||
TEST(MaterialLoader, GetResourceType) {
|
||||
MaterialLoader loader;
|
||||
EXPECT_EQ(loader.GetResourceType(), ResourceType::Material);
|
||||
@@ -235,6 +295,135 @@ TEST(MaterialLoader, LoadMaterialWithShaderManifestResolvesShaderPass) {
|
||||
fs::remove_all(shaderRoot);
|
||||
}
|
||||
|
||||
TEST(MaterialLoader, LoadMaterialWithPropertiesObjectAppliesTypedOverrides) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_override_test";
|
||||
fs::remove_all(rootPath);
|
||||
|
||||
const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath);
|
||||
ASSERT_FALSE(shaderPath.empty());
|
||||
const fs::path materialPath = rootPath / "override.material";
|
||||
|
||||
WriteTextFile(
|
||||
materialPath,
|
||||
"{\n"
|
||||
" \"shader\": \"" + shaderPath.generic_string() + "\",\n"
|
||||
" \"shaderPass\": \"ForwardLit\",\n"
|
||||
" \"properties\": {\n"
|
||||
" \"_BaseColor\": [0.2, 0.4, 0.6, 0.8],\n"
|
||||
" \"_Metallic\": 0.15,\n"
|
||||
" \"_Mode\": 5\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
|
||||
MaterialLoader loader;
|
||||
LoadResult result = loader.Load(materialPath.generic_string().c_str());
|
||||
ASSERT_TRUE(result);
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
auto* material = static_cast<Material*>(result.resource);
|
||||
ASSERT_NE(material, nullptr);
|
||||
ASSERT_NE(material->GetShader(), nullptr);
|
||||
EXPECT_EQ(material->GetShaderPass(), "ForwardLit");
|
||||
EXPECT_EQ(material->GetFloat4("_BaseColor"), XCEngine::Math::Vector4(0.2f, 0.4f, 0.6f, 0.8f));
|
||||
EXPECT_FLOAT_EQ(material->GetFloat("_Metallic"), 0.15f);
|
||||
EXPECT_EQ(material->GetInt("_Mode"), 5);
|
||||
EXPECT_TRUE(material->HasProperty("_MainTex"));
|
||||
EXPECT_EQ(material->GetTextureBindingCount(), 0u);
|
||||
|
||||
delete material;
|
||||
fs::remove_all(rootPath);
|
||||
}
|
||||
|
||||
TEST(MaterialLoader, RejectsUnknownPropertyAgainstShaderSchema) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_unknown_test";
|
||||
fs::remove_all(rootPath);
|
||||
|
||||
const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath);
|
||||
ASSERT_FALSE(shaderPath.empty());
|
||||
const fs::path materialPath = rootPath / "unknown_property.material";
|
||||
|
||||
WriteTextFile(
|
||||
materialPath,
|
||||
"{\n"
|
||||
" \"shader\": \"" + shaderPath.generic_string() + "\",\n"
|
||||
" \"properties\": {\n"
|
||||
" \"_Unknown\": 1.0\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
|
||||
MaterialLoader loader;
|
||||
LoadResult result = loader.Load(materialPath.generic_string().c_str());
|
||||
EXPECT_FALSE(result);
|
||||
|
||||
fs::remove_all(rootPath);
|
||||
}
|
||||
|
||||
TEST(MaterialLoader, RejectsTypeMismatchAgainstShaderSchema) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_mismatch_test";
|
||||
fs::remove_all(rootPath);
|
||||
|
||||
const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath);
|
||||
ASSERT_FALSE(shaderPath.empty());
|
||||
const fs::path materialPath = rootPath / "type_mismatch.material";
|
||||
|
||||
WriteTextFile(
|
||||
materialPath,
|
||||
"{\n"
|
||||
" \"shader\": \"" + shaderPath.generic_string() + "\",\n"
|
||||
" \"properties\": {\n"
|
||||
" \"_BaseColor\": 1.0\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
|
||||
MaterialLoader loader;
|
||||
LoadResult result = loader.Load(materialPath.generic_string().c_str());
|
||||
EXPECT_FALSE(result);
|
||||
|
||||
fs::remove_all(rootPath);
|
||||
}
|
||||
|
||||
TEST(MaterialLoader, LoadMaterialWithPropertiesObjectPreservesShaderDefaultsForOmittedValues) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_defaults_test";
|
||||
fs::remove_all(rootPath);
|
||||
|
||||
const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath);
|
||||
ASSERT_FALSE(shaderPath.empty());
|
||||
const fs::path materialPath = rootPath / "defaults.material";
|
||||
|
||||
WriteTextFile(
|
||||
materialPath,
|
||||
"{\n"
|
||||
" \"shader\": \"" + shaderPath.generic_string() + "\",\n"
|
||||
" \"properties\": {\n"
|
||||
" \"_Metallic\": 0.33\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
|
||||
MaterialLoader loader;
|
||||
LoadResult result = loader.Load(materialPath.generic_string().c_str());
|
||||
ASSERT_TRUE(result);
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
auto* material = static_cast<Material*>(result.resource);
|
||||
ASSERT_NE(material, nullptr);
|
||||
EXPECT_EQ(material->GetFloat4("_BaseColor"), XCEngine::Math::Vector4(1.0f, 0.5f, 0.25f, 1.0f));
|
||||
EXPECT_FLOAT_EQ(material->GetFloat("_Metallic"), 0.33f);
|
||||
EXPECT_EQ(material->GetInt("_Mode"), 2);
|
||||
EXPECT_TRUE(material->HasProperty("_MainTex"));
|
||||
EXPECT_EQ(material->GetTextureBindingCount(), 0u);
|
||||
|
||||
delete material;
|
||||
fs::remove_all(rootPath);
|
||||
}
|
||||
|
||||
TEST(MaterialLoader, RejectsUnknownRenderQueueName) {
|
||||
const std::filesystem::path materialPath =
|
||||
std::filesystem::current_path() / "material_loader_invalid_queue.material";
|
||||
@@ -380,6 +569,7 @@ TEST(MaterialLoader, ResourceManagerLoadsProjectMaterialTextureAsLazyDependency)
|
||||
ASSERT_TRUE(materialHandle.IsValid());
|
||||
ASSERT_EQ(materialHandle->GetTextureBindingCount(), 1u);
|
||||
EXPECT_EQ(materialHandle->GetTextureBindingName(0), "baseColorTexture");
|
||||
EXPECT_TRUE(materialHandle->GetTextureBindingAssetRef(0).IsValid());
|
||||
EXPECT_EQ(
|
||||
fs::path(materialHandle->GetTextureBindingPath(0).CStr()).lexically_normal().generic_string(),
|
||||
(projectRoot / "Assets" / "checker.bmp").lexically_normal().generic_string());
|
||||
@@ -542,6 +732,9 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) {
|
||||
assetsDir / "checker.bmp",
|
||||
fs::copy_options::overwrite_existing);
|
||||
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
manager.RefreshProjectAssets();
|
||||
|
||||
{
|
||||
std::ofstream output(materialArtifactPath, std::ios::binary | std::ios::trunc);
|
||||
ASSERT_TRUE(output.is_open());
|
||||
@@ -558,11 +751,18 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) {
|
||||
header.textureBindingCount = 1;
|
||||
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
|
||||
WriteArtifactString(output, "baseColorTexture");
|
||||
WriteArtifactString(output, "Assets/checker.bmp");
|
||||
}
|
||||
AssetRef textureRef;
|
||||
ASSERT_TRUE(manager.TryGetAssetRef("Assets/checker.bmp", ResourceType::Texture, textureRef));
|
||||
ASSERT_TRUE(textureRef.IsValid());
|
||||
const String encodedTextureRef =
|
||||
textureRef.assetGuid.ToString() + "," +
|
||||
String(std::to_string(textureRef.localID).c_str()) + "," +
|
||||
String(std::to_string(static_cast<int>(textureRef.resourceType)).c_str());
|
||||
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
WriteArtifactString(output, "baseColorTexture");
|
||||
WriteArtifactString(output, encodedTextureRef);
|
||||
WriteArtifactString(output, "");
|
||||
}
|
||||
|
||||
MaterialLoader loader;
|
||||
LoadResult result = loader.Load("Library/Manual/test.xcmat");
|
||||
@@ -572,9 +772,8 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) {
|
||||
auto* material = static_cast<Material*>(result.resource);
|
||||
ASSERT_NE(material, nullptr);
|
||||
EXPECT_EQ(material->GetTextureBindingCount(), 1u);
|
||||
EXPECT_EQ(
|
||||
fs::path(material->GetTextureBindingPath(0).CStr()).lexically_normal().generic_string(),
|
||||
(projectRoot / "Assets" / "checker.bmp").lexically_normal().generic_string());
|
||||
EXPECT_TRUE(material->GetTextureBindingAssetRef(0).IsValid());
|
||||
EXPECT_TRUE(material->GetTextureBindingPath(0).Empty());
|
||||
|
||||
const ResourceHandle<Texture> initialTexture = material->GetTexture("baseColorTexture");
|
||||
EXPECT_FALSE(initialTexture.IsValid());
|
||||
@@ -585,6 +784,9 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) {
|
||||
ASSERT_TRUE(loadedTexture.IsValid());
|
||||
EXPECT_EQ(loadedTexture->GetWidth(), 2u);
|
||||
EXPECT_EQ(loadedTexture->GetHeight(), 2u);
|
||||
EXPECT_EQ(
|
||||
fs::path(material->GetTextureBindingPath(0).CStr()).lexically_normal().generic_string(),
|
||||
fs::path("Assets/checker.bmp").lexically_normal().generic_string());
|
||||
|
||||
delete material;
|
||||
manager.SetResourceRoot("");
|
||||
|
||||
Reference in New Issue
Block a user