Formalize material schema and constant layout contract
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user