Tighten material schema-driven binding path

This commit is contained in:
2026-04-04 17:02:56 +08:00
parent 672f25f9b7
commit a74c25b5ae
3 changed files with 185 additions and 6 deletions

View File

@@ -479,9 +479,14 @@ Unity-like Shader Authoring (.shader)
- `forward-lit / unlit / object-id / depth-only / shadow-caster` 五个 builtin `.shader` 入口已全部切到 Unity-like authoring - `forward-lit / unlit / object-id / depth-only / shadow-caster` 五个 builtin `.shader` 入口已全部切到 Unity-like authoring
- stage 源文件、builtin shader 路径与 renderer 消费 contract 保持不变,迁移只发生在 authoring 入口层 - stage 源文件、builtin shader 路径与 renderer 消费 contract 保持不变,迁移只发生在 authoring 入口层
- 补充 `DepthOnly / ShadowCaster` builtin shader loader 覆盖,确保五类 builtin pass 都经过新 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` 中新增 authoring 直载与 artifact/reimport 覆盖
- 已验证:`shader_tests` 31/31 通过builtin `ForwardLit / Unlit / ObjectId / DepthOnly / ShadowCaster` 全部通过加载与 backend variant 覆盖 - 已验证:`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 主线可以正式阶段性收口
当前阶段明确不做: 当前阶段明确不做:

View File

@@ -620,6 +620,93 @@ bool TryParseFloatListText(const std::string& text,
return outCount > 0; 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<unsigned char>(value[index]);
if (std::isalnum(ch) != 0) {
normalized.push_back(static_cast<char>(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, bool TryApplySchemaMaterialProperty(Material* material,
const ShaderPropertyDesc& shaderProperty, const ShaderPropertyDesc& shaderProperty,
const std::string& rawValue, const std::string& rawValue,
@@ -767,7 +854,7 @@ bool TryParseMaterialPropertiesObject(const std::string& objectText, Material* m
const Shader* shader = material->GetShader(); const Shader* shader = material->GetShader();
const ShaderPropertyDesc* shaderProperty = const ShaderPropertyDesc* shaderProperty =
shader != nullptr ? shader->FindProperty(propertyName) : nullptr; ResolveShaderPropertyForMaterialKey(shader, propertyName, rawType);
if (shader != nullptr && shaderProperty == nullptr) { if (shader != nullptr && shaderProperty == nullptr) {
return false; return false;
} }
@@ -943,8 +1030,17 @@ bool TryApplyTexturePath(Material* material,
return false; 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( material->SetTexturePath(
textureName, resolvedPropertyName,
ResolveSourceDependencyPath(texturePath, material->GetPath())); ResolveSourceDependencyPath(texturePath, material->GetPath()));
return true; return true;
} }
@@ -978,7 +1074,9 @@ bool TryParseMaterialTextureBindings(const std::string& jsonText, Material* mate
return false; return false;
} }
TryApplyTexturePath(material, Containers::String(key), texturePath); if (!TryApplyTexturePath(material, Containers::String(key), texturePath)) {
return false;
}
} }
if (HasKey(jsonText, "textures")) { if (HasKey(jsonText, "textures")) {
@@ -987,13 +1085,19 @@ bool TryParseMaterialTextureBindings(const std::string& jsonText, Material* mate
return false; return false;
} }
bool appliedAllBindings = true;
if (!TryParseStringMapObject( if (!TryParseStringMapObject(
texturesObject, texturesObject,
[material](const Containers::String& name, const Containers::String& value) { [material, &appliedAllBindings](const Containers::String& name, const Containers::String& value) {
TryApplyTexturePath(material, name, value); if (appliedAllBindings) {
appliedAllBindings = TryApplyTexturePath(material, name, value);
}
})) { })) {
return false; return false;
} }
if (!appliedAllBindings) {
return false;
}
} }
return true; return true;

View File

@@ -424,6 +424,76 @@ TEST(MaterialLoader, LoadMaterialWithPropertiesObjectPreservesShaderDefaultsForO
fs::remove_all(rootPath); 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<Material*>(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) { TEST(MaterialLoader, RejectsUnknownRenderQueueName) {
const std::filesystem::path materialPath = const std::filesystem::path materialPath =
std::filesystem::current_path() / "material_loader_invalid_queue.material"; std::filesystem::current_path() / "material_loader_invalid_queue.material";