Formalize material schema and constant layout contract

This commit is contained in:
2026-04-03 16:49:30 +08:00
parent 052ac28aa3
commit 03bd755e0a
10 changed files with 1841 additions and 87 deletions

View File

@@ -12,6 +12,7 @@
#include <functional>
#include <fstream>
#include <string>
#include <vector>
namespace XCEngine {
namespace Resources {
@@ -284,6 +285,21 @@ bool TryParseBoolValue(const std::string& json, const char* key, bool& outValue)
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] != '{') {
@@ -329,6 +345,461 @@ bool TryExtractObject(const std::string& json, const char* key, std::string& out
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;
}
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 =
shader != nullptr ? shader->FindProperty(propertyName) : nullptr;
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") {
@@ -912,7 +1383,7 @@ LoadResult LoadMaterialArtifact(const Containers::String& path) {
}
const std::string magic(fileHeader.magic, fileHeader.magic + 7);
if (magic != "XCMAT01") {
if (magic != "XCMAT02") {
return LoadResult("Invalid material artifact magic: " + path);
}
@@ -984,14 +1455,23 @@ LoadResult LoadMaterialArtifact(const Containers::String& path) {
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);
}
if (!texturePath.Empty()) {
material->SetTexturePath(bindingName, ResolveArtifactDependencyPath(texturePath, 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);
}
}
@@ -1123,6 +1603,14 @@ bool MaterialLoader::ParseMaterialData(const Containers::Array<Core::uint8>& dat
}
}
if (HasKey(jsonText, "properties")) {
std::string propertiesObject;
if (!TryExtractObject(jsonText, "properties", propertiesObject) ||
!TryParseMaterialPropertiesObject(propertiesObject, material)) {
return false;
}
}
if (!TryParseMaterialTextureBindings(jsonText, material)) {
return false;
}