2026-03-24 16:14:05 +08:00
|
|
|
#include <XCEngine/Resources/Material/MaterialLoader.h>
|
2026-04-02 03:03:36 +08:00
|
|
|
#include <XCEngine/Resources/BuiltinResources.h>
|
2026-03-24 16:14:05 +08:00
|
|
|
#include <XCEngine/Core/Asset/ResourceManager.h>
|
|
|
|
|
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
2026-03-27 00:30:49 +08:00
|
|
|
#include <XCEngine/Resources/Shader/ShaderLoader.h>
|
|
|
|
|
|
|
|
|
|
#include <cctype>
|
|
|
|
|
#include <cstdlib>
|
2026-04-02 03:03:36 +08:00
|
|
|
#include <filesystem>
|
2026-03-27 00:30:49 +08:00
|
|
|
#include <string>
|
2026-03-17 22:32:27 +08:00
|
|
|
|
|
|
|
|
namespace XCEngine {
|
|
|
|
|
namespace Resources {
|
|
|
|
|
|
2026-03-27 00:30:49 +08:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
std::string ToStdString(const Containers::Array<Core::uint8>& data) {
|
|
|
|
|
return std::string(reinterpret_cast<const char*>(data.Data()), data.Size());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 12:18:04 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 00:30:49 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 12:18:04 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 00:30:49 +08:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 03:03:36 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 00:30:49 +08:00
|
|
|
} // namespace
|
|
|
|
|
|
2026-03-17 22:32:27 +08:00
|
|
|
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");
|
|
|
|
|
return extensions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool MaterialLoader::CanLoad(const Containers::String& path) const {
|
2026-04-02 03:03:36 +08:00
|
|
|
if (IsBuiltinMaterialPath(path)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 22:32:27 +08:00
|
|
|
Containers::String ext = GetExtension(path);
|
|
|
|
|
return ext == "mat" || ext == "material" || ext == "json";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LoadResult MaterialLoader::Load(const Containers::String& path, const ImportSettings* settings) {
|
2026-03-27 00:30:49 +08:00
|
|
|
(void)settings;
|
|
|
|
|
|
2026-04-02 03:03:36 +08:00
|
|
|
if (IsBuiltinMaterialPath(path)) {
|
|
|
|
|
return CreateBuiltinMaterialResource(path);
|
2026-03-17 22:32:27 +08:00
|
|
|
}
|
2026-03-27 00:30:49 +08:00
|
|
|
|
2026-04-02 03:03:36 +08:00
|
|
|
Containers::Array<Core::uint8> data = ReadFileData(path);
|
2026-03-17 22:32:27 +08:00
|
|
|
Material* material = new Material();
|
|
|
|
|
material->m_path = path;
|
|
|
|
|
material->m_name = path;
|
|
|
|
|
material->m_guid = ResourceGUID::Generate(path);
|
2026-03-27 00:30:49 +08:00
|
|
|
|
2026-04-02 03:03:36 +08:00
|
|
|
if (data.Empty() && !MaterialFileExists(path)) {
|
|
|
|
|
delete material;
|
|
|
|
|
return LoadResult("Failed to read material file: " + path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!data.Empty() && !ParseMaterialData(data, material)) {
|
2026-03-27 00:30:49 +08:00
|
|
|
delete material;
|
|
|
|
|
return LoadResult("Failed to parse material file: " + path);
|
2026-03-17 22:32:27 +08:00
|
|
|
}
|
2026-03-27 00:30:49 +08:00
|
|
|
|
2026-03-17 22:32:27 +08:00
|
|
|
material->m_isValid = true;
|
2026-03-27 00:30:49 +08:00
|
|
|
material->RecalculateMemorySize();
|
2026-03-17 22:32:27 +08:00
|
|
|
return LoadResult(material);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImportSettings* MaterialLoader::GetDefaultSettings() const {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool MaterialLoader::ParseMaterialData(const Containers::Array<Core::uint8>& data, Material* material) {
|
2026-03-27 00:30:49 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 12:18:04 +08:00
|
|
|
if (HasKey(jsonText, "renderState")) {
|
|
|
|
|
std::string renderStateObject;
|
|
|
|
|
if (!TryExtractObject(jsonText, "renderState", renderStateObject) ||
|
|
|
|
|
!TryParseRenderStateObject(renderStateObject, material)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 22:32:27 +08:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace Resources
|
|
|
|
|
} // namespace XCEngine
|