Files
XCEngine/engine/src/Resources/Material/MaterialLoader.cpp

1727 lines
52 KiB
C++

#include <XCEngine/Resources/Material/MaterialLoader.h>
#include <XCEngine/Core/Asset/ArtifactFormats.h>
#include <XCEngine/Resources/BuiltinResources.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Resources/Shader/ShaderLoader.h>
#include <cctype>
#include <cstring>
#include <cstdlib>
#include <filesystem>
#include <functional>
#include <fstream>
#include <string>
#include <vector>
namespace XCEngine {
namespace Resources {
namespace {
std::string ToStdString(const Containers::Array<Core::uint8>& data) {
return std::string(reinterpret_cast<const char*>(data.Data()), data.Size());
}
Containers::Array<Core::uint8> ReadMaterialArtifactFileData(const Containers::String& path) {
Containers::Array<Core::uint8> 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_t>(size));
if (!file.read(reinterpret_cast<char*>(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<unsigned char>(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<unsigned char>(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<Core::int32>(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<LocalID>(std::stoull(text.substr(firstComma + 1, secondComma - firstComma - 1)));
outRef.resourceType = static_cast<ResourceType>(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<unsigned char>(text[last - 1])) != 0) {
--last;
}
return text.substr(first, last - first);
}
bool IsJsonValueTerminator(char ch) {
return std::isspace(static_cast<unsigned char>(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<unsigned char>(ch)) != 0) {
size_t endPos = valuePos + 1;
while (endPos < text.size()) {
const char current = text[endPos];
if (std::isdigit(static_cast<unsigned char>(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<unsigned char>(*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<unsigned char>(*endPtr)) == 0) {
return false;
}
++endPtr;
}
outValue = static_cast<Core::int32>(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<unsigned char>(*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<unsigned char>(*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<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,
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<Core::int32>(MaterialRenderQueue::Background);
return true;
}
if (normalized == "geometry" || normalized == "opaque") {
outQueue = static_cast<Core::int32>(MaterialRenderQueue::Geometry);
return true;
}
if (normalized == "alphatest" || normalized == "alpha_test") {
outQueue = static_cast<Core::int32>(MaterialRenderQueue::AlphaTest);
return true;
}
if (normalized == "transparent") {
outQueue = static_cast<Core::int32>(MaterialRenderQueue::Transparent);
return true;
}
if (normalized == "overlay") {
outQueue = static_cast<Core::int32>(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<void(const Containers::String&, const Containers::String&)>& 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<Core::uint8>(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<Shader> LoadShaderHandle(const Containers::String& shaderPath) {
ResourceHandle<Shader> shader = ResourceManager::Get().Load<Shader>(shaderPath);
if (shader.IsValid()) {
return shader;
}
ShaderLoader shaderLoader;
LoadResult shaderResult = shaderLoader.Load(shaderPath);
if (!shaderResult || shaderResult.resource == nullptr) {
return ResourceHandle<Shader>();
}
return ResourceHandle<Shader>(static_cast<Shader*>(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<Shader> LoadShaderHandle(const Containers::String& shaderPath);
template<typename T>
bool ReadMaterialArtifactValue(const Containers::Array<Core::uint8>& 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<Core::uint8>& 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<const char*>(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<Core::uint8> 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>();
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<Shader> 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<MaterialPropertyType>(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<Containers::String> MaterialLoader::GetSupportedExtensions() const {
Containers::Array<Containers::String> 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<Core::uint8> 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<Core::uint8>& 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<Shader> 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