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
|
- `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 主线可以正式阶段性收口
|
||||||
|
|
||||||
当前阶段明确不做:
|
当前阶段明确不做:
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user