From a74c25b5ae270fc3f4ed3e1d250c55761cad9723 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 4 Apr 2026 17:02:56 +0800 Subject: [PATCH] Tighten material schema-driven binding path --- docs/plan/Shader与Material系统下一阶段计划.md | 7 +- .../src/Resources/Material/MaterialLoader.cpp | 114 +++++++++++++++++- .../Material/test_material_loader.cpp | 70 +++++++++++ 3 files changed, 185 insertions(+), 6 deletions(-) diff --git a/docs/plan/Shader与Material系统下一阶段计划.md b/docs/plan/Shader与Material系统下一阶段计划.md index 9cb232b7..34b6d761 100644 --- a/docs/plan/Shader与Material系统下一阶段计划.md +++ b/docs/plan/Shader与Material系统下一阶段计划.md @@ -479,9 +479,14 @@ Unity-like Shader Authoring (.shader) - `forward-lit / unlit / object-id / depth-only / shadow-caster` 五个 builtin `.shader` 入口已全部切到 Unity-like authoring - stage 源文件、builtin shader 路径与 renderer 消费 contract 保持不变,迁移只发生在 authoring 入口层 - 补充 `DepthOnly / ShadowCaster` builtin shader loader 覆盖,确保五类 builtin pass 都经过新 authoring 路径验证 +- 已完成:Step 3 `material 主路径收紧到 imported shader schema` + - `MaterialLoader` 在存在 shader schema 时,`properties / textures` 中的旧 key 会先解析到 shader property semantic,再落到 shader 正式属性名 + - 旧的 `baseColor / baseColorTexture` 等兼容 key 仍可导入,但只作为兼容输入存在,不再直接污染 material 主执行路径 + - 当 material 绑定 shader schema 后,未知 texture binding 现在会被显式拒绝,不再静默忽略 - 已验证:`shader_tests` 中新增 authoring 直载与 artifact/reimport 覆盖 - 已验证:`shader_tests` 31/31 通过,builtin `ForwardLit / Unlit / ObjectId / DepthOnly / ShadowCaster` 全部通过加载与 backend variant 覆盖 -- 下一步:进入 Step 3,继续收紧 material 主路径,把 imported shader schema 变成 material 的正式主执行路径 +- 已验证:`material_tests` 53/53 通过,schema 驱动的 property/texture 映射与未知 binding 拒绝路径都已覆盖 +- 下一步:进入 Step 4,跑最小回归集收口,并确认当前 shader/material 主线可以正式阶段性收口 当前阶段明确不做: diff --git a/engine/src/Resources/Material/MaterialLoader.cpp b/engine/src/Resources/Material/MaterialLoader.cpp index 39e6a0f8..48987aea 100644 --- a/engine/src/Resources/Material/MaterialLoader.cpp +++ b/engine/src/Resources/Material/MaterialLoader.cpp @@ -620,6 +620,93 @@ bool TryParseFloatListText(const std::string& text, return outCount > 0; } +Containers::String NormalizeMaterialLookupToken(const Containers::String& value) { + std::string normalized; + normalized.reserve(value.Length()); + for (size_t index = 0; index < value.Length(); ++index) { + const unsigned char ch = static_cast(value[index]); + if (std::isalnum(ch) != 0) { + normalized.push_back(static_cast(std::tolower(ch))); + } + } + + return Containers::String(normalized.c_str()); +} + +const ShaderPropertyDesc* FindShaderPropertyBySemantic( + const Shader* shader, + const Containers::String& semantic) { + if (shader == nullptr || semantic.Empty()) { + return nullptr; + } + + const Containers::String normalizedSemantic = NormalizeMaterialLookupToken(semantic); + for (const ShaderPropertyDesc& property : shader->GetProperties()) { + if (NormalizeMaterialLookupToken(property.semantic) == normalizedSemantic) { + return &property; + } + } + + return nullptr; +} + +Containers::String ResolveLegacyMaterialSemanticAlias( + const Containers::String& propertyName, + JsonRawValueType rawType) { + const Containers::String normalizedName = NormalizeMaterialLookupToken(propertyName); + + if (rawType == JsonRawValueType::Array || + rawType == JsonRawValueType::Number) { + if (normalizedName == "basecolor" || + normalizedName == "color") { + return Containers::String("BaseColor"); + } + } + + if (rawType == JsonRawValueType::String) { + if (normalizedName == "basecolortexture" || + normalizedName == "maintex" || + normalizedName == "maintexture" || + normalizedName == "albedotexture" || + normalizedName == "texture") { + return Containers::String("BaseColorTexture"); + } + } + + return Containers::String(); +} + +const ShaderPropertyDesc* ResolveShaderPropertyForMaterialKey( + const Shader* shader, + const Containers::String& propertyName, + JsonRawValueType rawType) { + if (shader == nullptr || propertyName.Empty()) { + return nullptr; + } + + if (const ShaderPropertyDesc* property = shader->FindProperty(propertyName)) { + return property; + } + + const Containers::String normalizedName = NormalizeMaterialLookupToken(propertyName); + for (const ShaderPropertyDesc& property : shader->GetProperties()) { + if (NormalizeMaterialLookupToken(property.name) == normalizedName) { + return &property; + } + } + + if (const ShaderPropertyDesc* property = FindShaderPropertyBySemantic(shader, propertyName)) { + return property; + } + + const Containers::String semanticAlias = ResolveLegacyMaterialSemanticAlias(propertyName, rawType); + if (!semanticAlias.Empty()) { + return FindShaderPropertyBySemantic(shader, semanticAlias); + } + + return nullptr; +} + bool TryApplySchemaMaterialProperty(Material* material, const ShaderPropertyDesc& shaderProperty, const std::string& rawValue, @@ -767,7 +854,7 @@ bool TryParseMaterialPropertiesObject(const std::string& objectText, Material* m const Shader* shader = material->GetShader(); const ShaderPropertyDesc* shaderProperty = - shader != nullptr ? shader->FindProperty(propertyName) : nullptr; + ResolveShaderPropertyForMaterialKey(shader, propertyName, rawType); if (shader != nullptr && shaderProperty == nullptr) { return false; } @@ -943,8 +1030,17 @@ bool TryApplyTexturePath(Material* material, return false; } + const Shader* shader = material->GetShader(); + const ShaderPropertyDesc* shaderProperty = + ResolveShaderPropertyForMaterialKey(shader, textureName, JsonRawValueType::String); + if (shader != nullptr && shaderProperty == nullptr) { + return false; + } + const Containers::String resolvedPropertyName = + shaderProperty != nullptr ? shaderProperty->name : textureName; + material->SetTexturePath( - textureName, + resolvedPropertyName, ResolveSourceDependencyPath(texturePath, material->GetPath())); return true; } @@ -978,7 +1074,9 @@ bool TryParseMaterialTextureBindings(const std::string& jsonText, Material* mate return false; } - TryApplyTexturePath(material, Containers::String(key), texturePath); + if (!TryApplyTexturePath(material, Containers::String(key), texturePath)) { + return false; + } } if (HasKey(jsonText, "textures")) { @@ -987,13 +1085,19 @@ bool TryParseMaterialTextureBindings(const std::string& jsonText, Material* mate return false; } + bool appliedAllBindings = true; if (!TryParseStringMapObject( texturesObject, - [material](const Containers::String& name, const Containers::String& value) { - TryApplyTexturePath(material, name, value); + [material, &appliedAllBindings](const Containers::String& name, const Containers::String& value) { + if (appliedAllBindings) { + appliedAllBindings = TryApplyTexturePath(material, name, value); + } })) { return false; } + if (!appliedAllBindings) { + return false; + } } return true; diff --git a/tests/Resources/Material/test_material_loader.cpp b/tests/Resources/Material/test_material_loader.cpp index 6c9f3a54..0d6497c2 100644 --- a/tests/Resources/Material/test_material_loader.cpp +++ b/tests/Resources/Material/test_material_loader.cpp @@ -424,6 +424,76 @@ TEST(MaterialLoader, LoadMaterialWithPropertiesObjectPreservesShaderDefaultsForO fs::remove_all(rootPath); } +TEST(MaterialLoader, LoadMaterialMapsLegacySemanticKeysIntoShaderSchemaProperties) { + namespace fs = std::filesystem; + + const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_semantic_alias_test"; + fs::remove_all(rootPath); + fs::create_directories(rootPath); + + const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath); + ASSERT_FALSE(shaderPath.empty()); + const fs::path materialPath = rootPath / "semantic_alias.material"; + + WriteTextFile( + materialPath, + "{\n" + " \"shader\": \"" + shaderPath.generic_string() + "\",\n" + " \"properties\": {\n" + " \"baseColor\": [0.2, 0.4, 0.6, 0.8]\n" + " },\n" + " \"textures\": {\n" + " \"baseColorTexture\": \"checker.bmp\"\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(result.resource); + ASSERT_NE(material, nullptr); + ASSERT_NE(material->GetShader(), nullptr); + EXPECT_FALSE(material->HasProperty("baseColor")); + EXPECT_EQ(material->GetFloat4("_BaseColor"), XCEngine::Math::Vector4(0.2f, 0.4f, 0.6f, 0.8f)); + ASSERT_EQ(material->GetTextureBindingCount(), 1u); + EXPECT_EQ(material->GetTextureBindingName(0), "_MainTex"); + EXPECT_EQ( + fs::path(material->GetTextureBindingPath(0).CStr()).lexically_normal().generic_string(), + (rootPath / "checker.bmp").lexically_normal().generic_string()); + + delete material; + fs::remove_all(rootPath); +} + +TEST(MaterialLoader, RejectsUnknownTextureBindingAgainstShaderSchema) { + namespace fs = std::filesystem; + + const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_unknown_texture_test"; + fs::remove_all(rootPath); + fs::create_directories(rootPath); + + const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath); + ASSERT_FALSE(shaderPath.empty()); + const fs::path materialPath = rootPath / "unknown_texture.material"; + + WriteTextFile( + materialPath, + "{\n" + " \"shader\": \"" + shaderPath.generic_string() + "\",\n" + " \"textures\": {\n" + " \"unknownTexture\": \"checker.bmp\"\n" + " }\n" + "}\n"); + + MaterialLoader loader; + LoadResult result = loader.Load(materialPath.generic_string().c_str()); + EXPECT_FALSE(result); + + fs::remove_all(rootPath); +} + TEST(MaterialLoader, RejectsUnknownRenderQueueName) { const std::filesystem::path materialPath = std::filesystem::current_path() / "material_loader_invalid_queue.material";