#include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace XCEngine { namespace Resources { namespace { std::string ToStdString(const Containers::Array& data) { return std::string(reinterpret_cast(data.Data()), data.Size()); } Containers::Array ReadMaterialArtifactFileData(const Containers::String& path) { Containers::Array data; auto tryRead = [&data](const std::filesystem::path& filePath, bool& opened) { std::ifstream file(filePath, std::ios::binary | std::ios::ate); if (!file.is_open()) { opened = false; return; } opened = true; const std::streamsize size = file.tellg(); if (size <= 0) { return; } file.seekg(0, std::ios::beg); data.Resize(static_cast(size)); if (!file.read(reinterpret_cast(data.Data()), size)) { data.Clear(); } }; bool opened = false; const std::filesystem::path inputPath(path.CStr()); tryRead(inputPath, opened); if (opened || path.Empty() || inputPath.is_absolute()) { return data; } const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot(); if (resourceRoot.Empty()) { return data; } tryRead(std::filesystem::path(resourceRoot.CStr()) / inputPath, opened); return data; } Containers::String NormalizePathString(const std::filesystem::path& path) { return Containers::String(path.lexically_normal().generic_string().c_str()); } bool IsProjectRelativePath(const std::filesystem::path& path) { const std::string generic = path.generic_string(); return !generic.empty() && generic != "." && generic != ".." && generic.rfind("../", 0) != 0; } Containers::String ToProjectRelativeIfPossible(const std::filesystem::path& path) { const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot(); const std::filesystem::path normalizedPath = path.lexically_normal(); if (!resourceRoot.Empty() && normalizedPath.is_absolute()) { std::error_code ec; const std::filesystem::path relativePath = std::filesystem::relative(normalizedPath, std::filesystem::path(resourceRoot.CStr()), ec); if (!ec && IsProjectRelativePath(relativePath)) { return NormalizePathString(relativePath); } } return NormalizePathString(normalizedPath); } Containers::String ResolveSourceDependencyPath(const Containers::String& dependencyPath, const Containers::String& sourcePath) { if (dependencyPath.Empty()) { return dependencyPath; } std::filesystem::path dependencyFsPath(dependencyPath.CStr()); if (dependencyFsPath.is_absolute()) { return NormalizePathString(dependencyFsPath); } const std::filesystem::path sourceFsPath(sourcePath.CStr()); if (sourceFsPath.is_absolute()) { return ToProjectRelativeIfPossible(sourceFsPath.parent_path() / dependencyFsPath); } const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot(); if (!resourceRoot.Empty()) { return ToProjectRelativeIfPossible( std::filesystem::path(resourceRoot.CStr()) / sourceFsPath.parent_path() / dependencyFsPath); } return NormalizePathString(sourceFsPath.parent_path() / dependencyFsPath); } Containers::String ResolveArtifactDependencyPath(const Containers::String& dependencyPath, const Containers::String& ownerArtifactPath) { if (dependencyPath.Empty()) { return dependencyPath; } std::filesystem::path dependencyFsPath(dependencyPath.CStr()); if (dependencyFsPath.is_absolute() && std::filesystem::exists(dependencyFsPath)) { return NormalizePathString(dependencyFsPath); } if (std::filesystem::exists(dependencyFsPath)) { return NormalizePathString(dependencyFsPath); } const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot(); if (!resourceRoot.Empty()) { const std::filesystem::path projectRelativeCandidate = std::filesystem::path(resourceRoot.CStr()) / dependencyFsPath; if (std::filesystem::exists(projectRelativeCandidate)) { return NormalizePathString(projectRelativeCandidate); } } const std::filesystem::path ownerArtifactFsPath(ownerArtifactPath.CStr()); if (!ownerArtifactFsPath.is_absolute()) { return dependencyPath; } const std::filesystem::path ownerRelativeCandidate = ownerArtifactFsPath.parent_path() / dependencyFsPath; if (std::filesystem::exists(ownerRelativeCandidate)) { return NormalizePathString(ownerRelativeCandidate); } std::filesystem::path current = ownerArtifactFsPath.parent_path(); while (!current.empty()) { if (current.filename() == "Library") { const std::filesystem::path projectRoot = current.parent_path(); if (!projectRoot.empty()) { const std::filesystem::path projectRelativeCandidate = projectRoot / dependencyFsPath; if (std::filesystem::exists(projectRelativeCandidate)) { return NormalizePathString(projectRelativeCandidate); } } break; } const std::filesystem::path parent = current.parent_path(); if (parent == current) { break; } current = parent; } return dependencyPath; } size_t SkipWhitespace(const std::string& text, size_t pos) { while (pos < text.size() && std::isspace(static_cast(text[pos])) != 0) { ++pos; } return pos; } bool HasKey(const std::string& json, const char* key) { const std::string token = std::string("\"") + key + "\""; return json.find(token) != std::string::npos; } bool FindValueStart(const std::string& json, const char* key, size_t& valuePos) { const std::string token = std::string("\"") + key + "\""; const size_t keyPos = json.find(token); if (keyPos == std::string::npos) { return false; } const size_t colonPos = json.find(':', keyPos + token.length()); if (colonPos == std::string::npos) { return false; } valuePos = SkipWhitespace(json, colonPos + 1); return valuePos < json.size(); } bool ParseQuotedString(const std::string& text, size_t quotePos, Containers::String& outValue, size_t* nextPos = nullptr) { if (quotePos >= text.size() || text[quotePos] != '"') { return false; } std::string parsed; ++quotePos; while (quotePos < text.size()) { const char ch = text[quotePos]; if (ch == '\\') { if (quotePos + 1 >= text.size()) { return false; } parsed.push_back(text[quotePos + 1]); quotePos += 2; continue; } if (ch == '"') { outValue = parsed.c_str(); if (nextPos != nullptr) { *nextPos = quotePos + 1; } return true; } parsed.push_back(ch); ++quotePos; } return false; } bool TryParseStringValue(const std::string& json, const char* key, Containers::String& outValue) { size_t valuePos = 0; if (!FindValueStart(json, key, valuePos)) { return false; } return ParseQuotedString(json, valuePos, outValue); } bool TryParseIntValue(const std::string& json, const char* key, Core::int32& outValue) { size_t valuePos = 0; if (!FindValueStart(json, key, valuePos)) { return false; } if (valuePos >= json.size() || (json[valuePos] != '-' && std::isdigit(static_cast(json[valuePos])) == 0)) { return false; } char* endPtr = nullptr; const long parsed = std::strtol(json.c_str() + valuePos, &endPtr, 10); if (endPtr == json.c_str() + valuePos) { return false; } outValue = static_cast(parsed); return true; } bool TryParseBoolValue(const std::string& json, const char* key, bool& outValue) { size_t valuePos = 0; if (!FindValueStart(json, key, valuePos)) { return false; } if (json.compare(valuePos, 4, "true") == 0) { outValue = true; return true; } if (json.compare(valuePos, 5, "false") == 0) { outValue = false; return true; } return false; } bool TryDecodeAssetRef(const Containers::String& value, AssetRef& outRef) { const std::string text(value.CStr()); const size_t firstComma = text.find(','); const size_t secondComma = firstComma == std::string::npos ? std::string::npos : text.find(',', firstComma + 1); if (firstComma == std::string::npos || secondComma == std::string::npos) { return false; } outRef.assetGuid = AssetGUID::ParseOrDefault(Containers::String(text.substr(0, firstComma).c_str())); outRef.localID = static_cast(std::stoull(text.substr(firstComma + 1, secondComma - firstComma - 1))); outRef.resourceType = static_cast(std::stoi(text.substr(secondComma + 1))); return outRef.IsValid(); } bool TryExtractObject(const std::string& json, const char* key, std::string& outObject) { size_t valuePos = 0; if (!FindValueStart(json, key, valuePos) || valuePos >= json.size() || json[valuePos] != '{') { return false; } bool inString = false; bool escaped = false; int depth = 0; for (size_t pos = valuePos; pos < json.size(); ++pos) { const char ch = json[pos]; if (escaped) { escaped = false; continue; } if (ch == '\\') { escaped = true; continue; } if (ch == '"') { inString = !inString; continue; } if (inString) { continue; } if (ch == '{') { ++depth; } else if (ch == '}') { --depth; if (depth == 0) { outObject = json.substr(valuePos, pos - valuePos + 1); return true; } } } return false; } std::string TrimCopy(const std::string& text) { const size_t first = SkipWhitespace(text, 0); if (first >= text.size()) { return std::string(); } size_t last = text.size(); while (last > first && std::isspace(static_cast(text[last - 1])) != 0) { --last; } return text.substr(first, last - first); } bool IsJsonValueTerminator(char ch) { return std::isspace(static_cast(ch)) != 0 || ch == ',' || ch == '}' || ch == ']'; } bool TryExtractDelimitedText(const std::string& text, size_t valuePos, char openChar, char closeChar, std::string& outValue, size_t* nextPos = nullptr) { if (valuePos >= text.size() || text[valuePos] != openChar) { return false; } bool inString = false; bool escaped = false; int depth = 0; for (size_t pos = valuePos; pos < text.size(); ++pos) { const char ch = text[pos]; if (escaped) { escaped = false; continue; } if (ch == '\\') { escaped = true; continue; } if (ch == '"') { inString = !inString; continue; } if (inString) { continue; } if (ch == openChar) { ++depth; } else if (ch == closeChar) { --depth; if (depth == 0) { outValue = text.substr(valuePos, pos - valuePos + 1); if (nextPos != nullptr) { *nextPos = pos + 1; } return true; } } } return false; } enum class JsonRawValueType : Core::uint8 { Invalid = 0, String, Number, Bool, Array, Object, Null }; bool TryApplyTexturePath(Material* material, const Containers::String& textureName, const Containers::String& texturePath); bool TryExtractRawValue(const std::string& text, size_t valuePos, std::string& outValue, JsonRawValueType& outType, size_t* nextPos = nullptr) { outValue.clear(); outType = JsonRawValueType::Invalid; valuePos = SkipWhitespace(text, valuePos); if (valuePos >= text.size()) { return false; } const char ch = text[valuePos]; if (ch == '"') { Containers::String parsed; size_t endPos = 0; if (!ParseQuotedString(text, valuePos, parsed, &endPos)) { return false; } outValue = parsed.CStr(); outType = JsonRawValueType::String; if (nextPos != nullptr) { *nextPos = endPos; } return true; } if (ch == '{') { if (!TryExtractDelimitedText(text, valuePos, '{', '}', outValue, nextPos)) { return false; } outType = JsonRawValueType::Object; return true; } if (ch == '[') { if (!TryExtractDelimitedText(text, valuePos, '[', ']', outValue, nextPos)) { return false; } outType = JsonRawValueType::Array; return true; } auto tryExtractLiteral = [&](const char* literal, JsonRawValueType valueType) { const size_t literalLength = std::strlen(literal); if (text.compare(valuePos, literalLength, literal) != 0) { return false; } const size_t endPos = valuePos + literalLength; if (endPos < text.size() && !IsJsonValueTerminator(text[endPos])) { return false; } outValue = literal; outType = valueType; if (nextPos != nullptr) { *nextPos = endPos; } return true; }; if (tryExtractLiteral("true", JsonRawValueType::Bool) || tryExtractLiteral("false", JsonRawValueType::Bool) || tryExtractLiteral("null", JsonRawValueType::Null)) { return true; } if (ch == '-' || std::isdigit(static_cast(ch)) != 0) { size_t endPos = valuePos + 1; while (endPos < text.size()) { const char current = text[endPos]; if (std::isdigit(static_cast(current)) != 0 || current == '.' || current == 'e' || current == 'E' || current == '+' || current == '-') { ++endPos; continue; } break; } outValue = text.substr(valuePos, endPos - valuePos); outType = JsonRawValueType::Number; if (nextPos != nullptr) { *nextPos = endPos; } return true; } return false; } bool TryParseFloatText(const std::string& text, float& outValue) { const std::string trimmed = TrimCopy(text); if (trimmed.empty()) { return false; } char* endPtr = nullptr; outValue = std::strtof(trimmed.c_str(), &endPtr); if (endPtr == trimmed.c_str()) { return false; } while (*endPtr != '\0') { if (std::isspace(static_cast(*endPtr)) == 0) { return false; } ++endPtr; } return true; } bool TryParseIntText(const std::string& text, Core::int32& outValue) { const std::string trimmed = TrimCopy(text); if (trimmed.empty()) { return false; } char* endPtr = nullptr; const long parsed = std::strtol(trimmed.c_str(), &endPtr, 10); if (endPtr == trimmed.c_str()) { return false; } while (*endPtr != '\0') { if (std::isspace(static_cast(*endPtr)) == 0) { return false; } ++endPtr; } outValue = static_cast(parsed); return true; } bool TryParseFloatListText(const std::string& text, float* outValues, size_t maxValues, size_t& outCount) { outCount = 0; std::string trimmed = TrimCopy(text); if (trimmed.empty()) { return false; } if ((trimmed.front() == '[' && trimmed.back() == ']') || (trimmed.front() == '(' && trimmed.back() == ')') || (trimmed.front() == '{' && trimmed.back() == '}')) { trimmed = trimmed.substr(1, trimmed.size() - 2); } const char* cursor = trimmed.c_str(); while (*cursor != '\0' && outCount < maxValues) { while (*cursor != '\0' && (std::isspace(static_cast(*cursor)) != 0 || *cursor == ',')) { ++cursor; } if (*cursor == '\0') { break; } char* endPtr = nullptr; const float parsed = std::strtof(cursor, &endPtr); if (endPtr == cursor) { return false; } outValues[outCount++] = parsed; cursor = endPtr; } while (*cursor != '\0') { if (std::isspace(static_cast(*cursor)) == 0 && *cursor != ',') { return false; } ++cursor; } 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, JsonRawValueType rawType) { if (material == nullptr || shaderProperty.name.Empty()) { return false; } switch (shaderProperty.type) { case ShaderPropertyType::Float: case ShaderPropertyType::Range: { float value = 0.0f; if (rawType != JsonRawValueType::Number || !TryParseFloatText(rawValue, value)) { return false; } material->SetFloat(shaderProperty.name, value); return true; } case ShaderPropertyType::Int: { Core::int32 value = 0; if (rawType != JsonRawValueType::Number || !TryParseIntText(rawValue, value)) { return false; } material->SetInt(shaderProperty.name, value); return true; } case ShaderPropertyType::Vector: case ShaderPropertyType::Color: { float values[4] = {}; size_t count = 0; if (rawType != JsonRawValueType::Array || !TryParseFloatListText(rawValue, values, 4, count) || count > 4) { return false; } material->SetFloat4( shaderProperty.name, Math::Vector4( values[0], count > 1 ? values[1] : 0.0f, count > 2 ? values[2] : 0.0f, count > 3 ? values[3] : 0.0f)); return true; } case ShaderPropertyType::Texture2D: case ShaderPropertyType::TextureCube: return rawType == JsonRawValueType::String && TryApplyTexturePath(material, shaderProperty.name, Containers::String(rawValue.c_str())); default: return false; } } bool TryApplyInferredMaterialProperty(Material* material, const Containers::String& propertyName, const std::string& rawValue, JsonRawValueType rawType) { if (material == nullptr || propertyName.Empty()) { return false; } switch (rawType) { case JsonRawValueType::Number: { Core::int32 intValue = 0; if (TryParseIntText(rawValue, intValue)) { material->SetInt(propertyName, intValue); return true; } float floatValue = 0.0f; if (!TryParseFloatText(rawValue, floatValue)) { return false; } material->SetFloat(propertyName, floatValue); return true; } case JsonRawValueType::Bool: material->SetBool(propertyName, rawValue == "true"); return true; case JsonRawValueType::Array: { float values[4] = {}; size_t count = 0; if (!TryParseFloatListText(rawValue, values, 4, count) || count > 4) { return false; } switch (count) { case 1: material->SetFloat(propertyName, values[0]); return true; case 2: material->SetFloat2(propertyName, Math::Vector2(values[0], values[1])); return true; case 3: material->SetFloat3(propertyName, Math::Vector3(values[0], values[1], values[2])); return true; case 4: material->SetFloat4(propertyName, Math::Vector4(values[0], values[1], values[2], values[3])); return true; default: return false; } } default: return false; } } bool TryParseMaterialPropertiesObject(const std::string& objectText, Material* material) { if (material == nullptr || objectText.empty() || objectText.front() != '{' || objectText.back() != '}') { return false; } size_t pos = 1; while (pos < objectText.size()) { pos = SkipWhitespace(objectText, pos); if (pos >= objectText.size()) { return false; } if (objectText[pos] == '}') { return true; } Containers::String propertyName; if (!ParseQuotedString(objectText, pos, propertyName, &pos) || propertyName.Empty()) { return false; } pos = SkipWhitespace(objectText, pos); if (pos >= objectText.size() || objectText[pos] != ':') { return false; } std::string rawValue; JsonRawValueType rawType = JsonRawValueType::Invalid; pos = SkipWhitespace(objectText, pos + 1); if (!TryExtractRawValue(objectText, pos, rawValue, rawType, &pos)) { return false; } const Shader* shader = material->GetShader(); const ShaderPropertyDesc* shaderProperty = ResolveShaderPropertyForMaterialKey(shader, propertyName, rawType); if (shader != nullptr && shaderProperty == nullptr) { return false; } if (shaderProperty != nullptr) { if (!TryApplySchemaMaterialProperty(material, *shaderProperty, rawValue, rawType)) { return false; } } else if (!TryApplyInferredMaterialProperty(material, propertyName, rawValue, rawType)) { return false; } pos = SkipWhitespace(objectText, pos); if (pos >= objectText.size()) { return false; } if (objectText[pos] == ',') { ++pos; continue; } if (objectText[pos] == '}') { return true; } return false; } return false; } bool TryParseRenderQueueName(const Containers::String& queueName, Core::int32& outQueue) { const Containers::String normalized = queueName.ToLower(); if (normalized == "background") { outQueue = static_cast(MaterialRenderQueue::Background); return true; } if (normalized == "geometry" || normalized == "opaque") { outQueue = static_cast(MaterialRenderQueue::Geometry); return true; } if (normalized == "alphatest" || normalized == "alpha_test") { outQueue = static_cast(MaterialRenderQueue::AlphaTest); return true; } if (normalized == "transparent") { outQueue = static_cast(MaterialRenderQueue::Transparent); return true; } if (normalized == "overlay") { outQueue = static_cast(MaterialRenderQueue::Overlay); return true; } return false; } bool TryParseTagMap(const std::string& objectText, Material* material) { if (material == nullptr || objectText.empty() || objectText.front() != '{' || objectText.back() != '}') { return false; } size_t pos = 1; while (pos < objectText.size()) { pos = SkipWhitespace(objectText, pos); if (pos >= objectText.size()) { return false; } if (objectText[pos] == '}') { return true; } Containers::String key; if (!ParseQuotedString(objectText, pos, key, &pos)) { return false; } pos = SkipWhitespace(objectText, pos); if (pos >= objectText.size() || objectText[pos] != ':') { return false; } pos = SkipWhitespace(objectText, pos + 1); Containers::String value; if (!ParseQuotedString(objectText, pos, value, &pos)) { return false; } material->SetTag(key, value); pos = SkipWhitespace(objectText, pos); if (pos >= objectText.size()) { return false; } if (objectText[pos] == ',') { ++pos; continue; } if (objectText[pos] == '}') { return true; } return false; } return false; } bool TryParseStringMapObject( const std::string& objectText, const std::function& onEntry) { if (!onEntry || objectText.empty() || objectText.front() != '{' || objectText.back() != '}') { return false; } size_t pos = 1; while (pos < objectText.size()) { pos = SkipWhitespace(objectText, pos); if (pos >= objectText.size()) { return false; } if (objectText[pos] == '}') { return true; } Containers::String key; if (!ParseQuotedString(objectText, pos, key, &pos)) { return false; } pos = SkipWhitespace(objectText, pos); if (pos >= objectText.size() || objectText[pos] != ':') { return false; } pos = SkipWhitespace(objectText, pos + 1); Containers::String value; if (!ParseQuotedString(objectText, pos, value, &pos)) { return false; } onEntry(key, value); pos = SkipWhitespace(objectText, pos); if (pos >= objectText.size()) { return false; } if (objectText[pos] == ',') { ++pos; continue; } if (objectText[pos] == '}') { return true; } return false; } return false; } bool TryApplyTexturePath(Material* material, const Containers::String& textureName, const Containers::String& texturePath) { if (material == nullptr || textureName.Empty() || texturePath.Empty()) { 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( resolvedPropertyName, ResolveSourceDependencyPath(texturePath, material->GetPath())); return true; } bool TryParseMaterialTextureBindings(const std::string& jsonText, Material* material) { if (material == nullptr) { return false; } static const char* const kKnownTextureKeys[] = { "baseColorTexture", "_BaseColorTexture", "_MainTex", "normalTexture", "_BumpMap", "specularTexture", "emissiveTexture", "metallicTexture", "roughnessTexture", "occlusionTexture", "opacityTexture" }; for (const char* key : kKnownTextureKeys) { if (!HasKey(jsonText, key)) { continue; } Containers::String texturePath; if (!TryParseStringValue(jsonText, key, texturePath)) { return false; } if (!TryApplyTexturePath(material, Containers::String(key), texturePath)) { return false; } } if (HasKey(jsonText, "textures")) { std::string texturesObject; if (!TryExtractObject(jsonText, "textures", texturesObject)) { return false; } bool appliedAllBindings = true; if (!TryParseStringMapObject( texturesObject, [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; } bool TryParseCullMode(const Containers::String& value, MaterialCullMode& outMode) { const Containers::String normalized = value.Trim().ToLower(); if (normalized == "none" || normalized == "off") { outMode = MaterialCullMode::None; return true; } if (normalized == "front") { outMode = MaterialCullMode::Front; return true; } if (normalized == "back") { outMode = MaterialCullMode::Back; return true; } return false; } bool TryParseComparisonFunc(const Containers::String& value, MaterialComparisonFunc& outFunc) { const Containers::String normalized = value.Trim().ToLower(); if (normalized == "never") { outFunc = MaterialComparisonFunc::Never; return true; } if (normalized == "less") { outFunc = MaterialComparisonFunc::Less; return true; } if (normalized == "equal") { outFunc = MaterialComparisonFunc::Equal; return true; } if (normalized == "lessequal" || normalized == "less_equal" || normalized == "lequal") { outFunc = MaterialComparisonFunc::LessEqual; return true; } if (normalized == "greater") { outFunc = MaterialComparisonFunc::Greater; return true; } if (normalized == "notequal" || normalized == "not_equal") { outFunc = MaterialComparisonFunc::NotEqual; return true; } if (normalized == "greaterequal" || normalized == "greater_equal" || normalized == "gequal") { outFunc = MaterialComparisonFunc::GreaterEqual; return true; } if (normalized == "always") { outFunc = MaterialComparisonFunc::Always; return true; } return false; } bool TryParseBlendFactor(const Containers::String& value, MaterialBlendFactor& outFactor) { const Containers::String normalized = value.Trim().ToLower(); if (normalized == "zero") { outFactor = MaterialBlendFactor::Zero; return true; } if (normalized == "one") { outFactor = MaterialBlendFactor::One; return true; } if (normalized == "srccolor" || normalized == "src_color") { outFactor = MaterialBlendFactor::SrcColor; return true; } if (normalized == "invsrccolor" || normalized == "inv_src_color" || normalized == "one_minus_src_color") { outFactor = MaterialBlendFactor::InvSrcColor; return true; } if (normalized == "srcalpha" || normalized == "src_alpha") { outFactor = MaterialBlendFactor::SrcAlpha; return true; } if (normalized == "invsrcalpha" || normalized == "inv_src_alpha" || normalized == "one_minus_src_alpha") { outFactor = MaterialBlendFactor::InvSrcAlpha; return true; } if (normalized == "dstalpha" || normalized == "dst_alpha") { outFactor = MaterialBlendFactor::DstAlpha; return true; } if (normalized == "invdstalpha" || normalized == "inv_dst_alpha" || normalized == "one_minus_dst_alpha") { outFactor = MaterialBlendFactor::InvDstAlpha; return true; } if (normalized == "dstcolor" || normalized == "dst_color") { outFactor = MaterialBlendFactor::DstColor; return true; } if (normalized == "invdstcolor" || normalized == "inv_dst_color" || normalized == "one_minus_dst_color") { outFactor = MaterialBlendFactor::InvDstColor; return true; } if (normalized == "srcalphasat" || normalized == "src_alpha_sat") { outFactor = MaterialBlendFactor::SrcAlphaSat; return true; } if (normalized == "blendfactor" || normalized == "blend_factor") { outFactor = MaterialBlendFactor::BlendFactor; return true; } if (normalized == "invblendfactor" || normalized == "inv_blend_factor" || normalized == "one_minus_blend_factor") { outFactor = MaterialBlendFactor::InvBlendFactor; return true; } if (normalized == "src1color" || normalized == "src1_color") { outFactor = MaterialBlendFactor::Src1Color; return true; } if (normalized == "invsrc1color" || normalized == "inv_src1_color" || normalized == "one_minus_src1_color") { outFactor = MaterialBlendFactor::InvSrc1Color; return true; } if (normalized == "src1alpha" || normalized == "src1_alpha") { outFactor = MaterialBlendFactor::Src1Alpha; return true; } if (normalized == "invsrc1alpha" || normalized == "inv_src1_alpha" || normalized == "one_minus_src1_alpha") { outFactor = MaterialBlendFactor::InvSrc1Alpha; return true; } return false; } bool TryParseBlendOp(const Containers::String& value, MaterialBlendOp& outOp) { const Containers::String normalized = value.Trim().ToLower(); if (normalized == "add") { outOp = MaterialBlendOp::Add; return true; } if (normalized == "subtract") { outOp = MaterialBlendOp::Subtract; return true; } if (normalized == "reversesubtract" || normalized == "reverse_subtract") { outOp = MaterialBlendOp::ReverseSubtract; return true; } if (normalized == "min") { outOp = MaterialBlendOp::Min; return true; } if (normalized == "max") { outOp = MaterialBlendOp::Max; return true; } return false; } bool TryParseRenderStateObject(const std::string& objectText, Material* material) { if (material == nullptr || objectText.empty() || objectText.front() != '{' || objectText.back() != '}') { return false; } MaterialRenderState renderState = material->GetRenderState(); Containers::String enumValue; if (HasKey(objectText, "cull")) { if (!TryParseStringValue(objectText, "cull", enumValue) || !TryParseCullMode(enumValue, renderState.cullMode)) { return false; } } bool boolValue = false; if (HasKey(objectText, "depthTest")) { if (!TryParseBoolValue(objectText, "depthTest", boolValue)) { return false; } renderState.depthTestEnable = boolValue; } if (HasKey(objectText, "depthWrite")) { if (!TryParseBoolValue(objectText, "depthWrite", boolValue)) { return false; } renderState.depthWriteEnable = boolValue; } if (HasKey(objectText, "zWrite")) { if (!TryParseBoolValue(objectText, "zWrite", boolValue)) { return false; } renderState.depthWriteEnable = boolValue; } if (HasKey(objectText, "blend")) { if (!TryParseBoolValue(objectText, "blend", boolValue)) { return false; } renderState.blendEnable = boolValue; } if (HasKey(objectText, "blendEnable")) { if (!TryParseBoolValue(objectText, "blendEnable", boolValue)) { return false; } renderState.blendEnable = boolValue; } if (HasKey(objectText, "depthFunc")) { if (!TryParseStringValue(objectText, "depthFunc", enumValue) || !TryParseComparisonFunc(enumValue, renderState.depthFunc)) { return false; } } if (HasKey(objectText, "zTest")) { if (!TryParseStringValue(objectText, "zTest", enumValue) || !TryParseComparisonFunc(enumValue, renderState.depthFunc)) { return false; } } if (HasKey(objectText, "colorWriteMask")) { Core::int32 colorWriteMask = 0; if (!TryParseIntValue(objectText, "colorWriteMask", colorWriteMask) || colorWriteMask < 0 || colorWriteMask > 0xF) { return false; } renderState.colorWriteMask = static_cast(colorWriteMask); } if (HasKey(objectText, "srcBlend")) { if (!TryParseStringValue(objectText, "srcBlend", enumValue) || !TryParseBlendFactor(enumValue, renderState.srcBlend)) { return false; } } if (HasKey(objectText, "dstBlend")) { if (!TryParseStringValue(objectText, "dstBlend", enumValue) || !TryParseBlendFactor(enumValue, renderState.dstBlend)) { return false; } } if (HasKey(objectText, "srcBlendAlpha")) { if (!TryParseStringValue(objectText, "srcBlendAlpha", enumValue) || !TryParseBlendFactor(enumValue, renderState.srcBlendAlpha)) { return false; } } if (HasKey(objectText, "dstBlendAlpha")) { if (!TryParseStringValue(objectText, "dstBlendAlpha", enumValue) || !TryParseBlendFactor(enumValue, renderState.dstBlendAlpha)) { return false; } } if (HasKey(objectText, "blendOp")) { if (!TryParseStringValue(objectText, "blendOp", enumValue) || !TryParseBlendOp(enumValue, renderState.blendOp)) { return false; } } if (HasKey(objectText, "blendOpAlpha")) { if (!TryParseStringValue(objectText, "blendOpAlpha", enumValue) || !TryParseBlendOp(enumValue, renderState.blendOpAlpha)) { return false; } } material->SetRenderState(renderState); return true; } ResourceHandle LoadShaderHandle(const Containers::String& shaderPath) { ResourceHandle shader = ResourceManager::Get().Load(shaderPath); if (shader.IsValid()) { return shader; } ShaderLoader shaderLoader; LoadResult shaderResult = shaderLoader.Load(shaderPath); if (!shaderResult || shaderResult.resource == nullptr) { return ResourceHandle(); } return ResourceHandle(static_cast(shaderResult.resource)); } bool MaterialFileExists(const Containers::String& path) { const std::filesystem::path inputPath(path.CStr()); if (std::filesystem::exists(inputPath)) { return true; } if (inputPath.is_absolute()) { return false; } const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot(); if (resourceRoot.Empty()) { return false; } return std::filesystem::exists(std::filesystem::path(resourceRoot.CStr()) / inputPath); } ResourceHandle LoadShaderHandle(const Containers::String& shaderPath); template bool ReadMaterialArtifactValue(const Containers::Array& data, size_t& offset, T& outValue) { if (offset + sizeof(T) > data.Size()) { return false; } std::memcpy(&outValue, data.Data() + offset, sizeof(T)); offset += sizeof(T); return true; } bool ReadMaterialArtifactString(const Containers::Array& data, size_t& offset, Containers::String& outValue) { Core::uint32 length = 0; if (!ReadMaterialArtifactValue(data, offset, length)) { return false; } if (length == 0) { outValue.Clear(); return true; } if (offset + length > data.Size()) { return false; } outValue = Containers::String( std::string(reinterpret_cast(data.Data() + offset), length).c_str()); offset += length; return true; } void ApplyMaterialProperty(Material& material, const MaterialProperty& property) { switch (property.type) { case MaterialPropertyType::Float: material.SetFloat(property.name, property.value.floatValue[0]); break; case MaterialPropertyType::Float2: material.SetFloat2( property.name, Math::Vector2(property.value.floatValue[0], property.value.floatValue[1])); break; case MaterialPropertyType::Float3: material.SetFloat3( property.name, Math::Vector3( property.value.floatValue[0], property.value.floatValue[1], property.value.floatValue[2])); break; case MaterialPropertyType::Float4: material.SetFloat4( property.name, Math::Vector4( property.value.floatValue[0], property.value.floatValue[1], property.value.floatValue[2], property.value.floatValue[3])); break; case MaterialPropertyType::Int: material.SetInt(property.name, property.value.intValue[0]); break; case MaterialPropertyType::Bool: material.SetBool(property.name, property.value.boolValue); break; default: break; } } LoadResult LoadMaterialArtifact(const Containers::String& path) { const Containers::Array data = ReadMaterialArtifactFileData(path); if (data.Empty()) { return LoadResult("Failed to read material artifact: " + path); } size_t offset = 0; MaterialArtifactFileHeader fileHeader; if (!ReadMaterialArtifactValue(data, offset, fileHeader)) { return LoadResult("Failed to parse material artifact header: " + path); } const std::string magic(fileHeader.magic, fileHeader.magic + 7); if (magic != "XCMAT02") { return LoadResult("Invalid material artifact magic: " + path); } auto material = std::make_unique(); material->m_path = path; material->m_name = path; material->m_guid = ResourceGUID::Generate(path); Containers::String materialName; Containers::String materialSourcePath; Containers::String shaderPath; Containers::String shaderPass; if (!ReadMaterialArtifactString(data, offset, materialName) || !ReadMaterialArtifactString(data, offset, materialSourcePath) || !ReadMaterialArtifactString(data, offset, shaderPath) || !ReadMaterialArtifactString(data, offset, shaderPass)) { return LoadResult("Failed to parse material artifact strings: " + path); } material->m_name = materialName.Empty() ? path : materialName; if (!materialSourcePath.Empty()) { material->m_path = materialSourcePath; material->m_guid = ResourceGUID::Generate(materialSourcePath); } if (!shaderPath.Empty()) { const ResourceHandle shaderHandle = LoadShaderHandle(shaderPath); if (shaderHandle.IsValid()) { material->SetShader(shaderHandle); } } if (!shaderPass.Empty()) { material->SetShaderPass(shaderPass); } MaterialArtifactHeader header; if (!ReadMaterialArtifactValue(data, offset, header)) { return LoadResult("Failed to parse material artifact body: " + path); } material->SetRenderQueue(header.renderQueue); material->SetRenderState(header.renderState); for (Core::uint32 tagIndex = 0; tagIndex < header.tagCount; ++tagIndex) { Containers::String tagName; Containers::String tagValue; if (!ReadMaterialArtifactString(data, offset, tagName) || !ReadMaterialArtifactString(data, offset, tagValue)) { return LoadResult("Failed to read material artifact tags: " + path); } material->SetTag(tagName, tagValue); } for (Core::uint32 propertyIndex = 0; propertyIndex < header.propertyCount; ++propertyIndex) { Containers::String propertyName; MaterialPropertyArtifact propertyArtifact; if (!ReadMaterialArtifactString(data, offset, propertyName) || !ReadMaterialArtifactValue(data, offset, propertyArtifact)) { return LoadResult("Failed to read material artifact properties: " + path); } MaterialProperty property; property.name = propertyName; property.type = static_cast(propertyArtifact.propertyType); property.value = propertyArtifact.value; ApplyMaterialProperty(*material, property); } for (Core::uint32 bindingIndex = 0; bindingIndex < header.textureBindingCount; ++bindingIndex) { Containers::String bindingName; Containers::String textureRefText; Containers::String texturePath; if (!ReadMaterialArtifactString(data, offset, bindingName) || !ReadMaterialArtifactString(data, offset, textureRefText) || !ReadMaterialArtifactString(data, offset, texturePath)) { return LoadResult("Failed to read material artifact texture bindings: " + path); } AssetRef textureRef; TryDecodeAssetRef(textureRefText, textureRef); const Containers::String resolvedTexturePath = texturePath.Empty() ? Containers::String() : ResolveArtifactDependencyPath(texturePath, path); if (textureRef.IsValid()) { material->SetTextureAssetRef(bindingName, textureRef, resolvedTexturePath); } else if (!resolvedTexturePath.Empty()) { material->SetTexturePath(bindingName, resolvedTexturePath); } } material->m_isValid = true; material->RecalculateMemorySize(); return LoadResult(material.release()); } } // namespace MaterialLoader::MaterialLoader() = default; MaterialLoader::~MaterialLoader() = default; Containers::Array MaterialLoader::GetSupportedExtensions() const { Containers::Array extensions; extensions.PushBack("mat"); extensions.PushBack("material"); extensions.PushBack("json"); extensions.PushBack("xcmat"); return extensions; } bool MaterialLoader::CanLoad(const Containers::String& path) const { if (IsBuiltinMaterialPath(path)) { return true; } Containers::String ext = GetExtension(path).ToLower(); return ext == "mat" || ext == "material" || ext == "json" || ext == "xcmat"; } LoadResult MaterialLoader::Load(const Containers::String& path, const ImportSettings* settings) { (void)settings; if (IsBuiltinMaterialPath(path)) { return CreateBuiltinMaterialResource(path); } const Containers::String ext = GetExtension(path).ToLower(); if (ext == "xcmat") { return LoadMaterialArtifact(path); } Containers::Array data = ReadFileData(path); Material* material = new Material(); material->m_path = path; material->m_name = path; material->m_guid = ResourceGUID::Generate(path); if (data.Empty() && !MaterialFileExists(path)) { delete material; return LoadResult("Failed to read material file: " + path); } if (!data.Empty() && !ParseMaterialData(data, material)) { delete material; return LoadResult("Failed to parse material file: " + path); } material->m_isValid = true; material->RecalculateMemorySize(); return LoadResult(material); } ImportSettings* MaterialLoader::GetDefaultSettings() const { return nullptr; } bool MaterialLoader::ParseMaterialData(const Containers::Array& data, Material* material) { if (material == nullptr) { return false; } const std::string jsonText = ToStdString(data); Containers::String shaderPath; if (HasKey(jsonText, "shader")) { if (!TryParseStringValue(jsonText, "shader", shaderPath)) { return false; } const ResourceHandle shaderHandle = LoadShaderHandle(shaderPath); if (shaderHandle.IsValid()) { material->SetShader(shaderHandle); } } Containers::String shaderPass; if (HasKey(jsonText, "shaderPass")) { if (!TryParseStringValue(jsonText, "shaderPass", shaderPass)) { return false; } material->SetShaderPass(shaderPass); } else if (HasKey(jsonText, "pass")) { if (!TryParseStringValue(jsonText, "pass", shaderPass)) { return false; } material->SetShaderPass(shaderPass); } if (HasKey(jsonText, "renderQueue")) { Core::int32 renderQueue = 0; if (TryParseIntValue(jsonText, "renderQueue", renderQueue)) { material->SetRenderQueue(renderQueue); } else { Containers::String renderQueueName; if (!TryParseStringValue(jsonText, "renderQueue", renderQueueName) || !TryParseRenderQueueName(renderQueueName, renderQueue)) { return false; } material->SetRenderQueue(renderQueue); } } if (HasKey(jsonText, "tags")) { std::string tagObject; if (!TryExtractObject(jsonText, "tags", tagObject) || !TryParseTagMap(tagObject, material)) { return false; } } if (HasKey(jsonText, "renderState")) { std::string renderStateObject; if (!TryExtractObject(jsonText, "renderState", renderStateObject) || !TryParseRenderStateObject(renderStateObject, material)) { return false; } } if (HasKey(jsonText, "properties")) { std::string propertiesObject; if (!TryExtractObject(jsonText, "properties", propertiesObject) || !TryParseMaterialPropertiesObject(propertiesObject, material)) { return false; } } if (!TryParseMaterialTextureBindings(jsonText, material)) { return false; } return true; } } // namespace Resources } // namespace XCEngine