Tighten material schema-driven binding path
This commit is contained in:
@@ -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 主线可以正式阶段性收口
|
||||
|
||||
当前阶段明确不做:
|
||||
|
||||
|
||||
@@ -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<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,
|
||||
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;
|
||||
|
||||
@@ -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<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) {
|
||||
const std::filesystem::path materialPath =
|
||||
std::filesystem::current_path() / "material_loader_invalid_queue.material";
|
||||
|
||||
Reference in New Issue
Block a user