1727 lines
52 KiB
C++
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
|