Refactor material inspector state IO

This commit is contained in:
2026-04-08 00:00:41 +08:00
parent 6289777c8e
commit 69cb80ccd4
9 changed files with 1458 additions and 769 deletions

View File

@@ -3,6 +3,7 @@
#include "Actions/EditorActions.h"
#include "Commands/ProjectCommands.h"
#include "InspectorPanel.h"
#include "MaterialInspectorMaterialStateIO.h"
#include "Core/AssetItem.h"
#include "Core/IEditorContext.h"
#include "Core/IProjectManager.h"
@@ -28,8 +29,6 @@
#include <imgui.h>
#include <cstring>
#include <string>
#include <unordered_map>
#include <unordered_set>
namespace XCEngine {
namespace Editor {
@@ -79,267 +78,6 @@ std::string TrimCopy(const std::string& value) {
return ProjectFileUtils::Trim(value);
}
std::string EscapeJsonString(const std::string& value) {
std::string escaped;
escaped.reserve(value.size());
for (const char ch : value) {
switch (ch) {
case '\\':
escaped += "\\\\";
break;
case '"':
escaped += "\\\"";
break;
case '\n':
escaped += "\\n";
break;
case '\r':
escaped += "\\r";
break;
case '\t':
escaped += "\\t";
break;
default:
escaped.push_back(ch);
break;
}
}
return escaped;
}
size_t SkipWhitespace(const std::string& text, size_t position) {
while (position < text.size() && std::isspace(static_cast<unsigned char>(text[position])) != 0) {
++position;
}
return position;
}
bool ParseQuotedString(
const std::string& text,
size_t quotePosition,
std::string& outValue,
size_t* nextPosition = nullptr) {
if (quotePosition >= text.size() || text[quotePosition] != '"') {
return false;
}
std::string parsed;
++quotePosition;
while (quotePosition < text.size()) {
const char ch = text[quotePosition];
if (ch == '\\') {
if (quotePosition + 1 >= text.size()) {
return false;
}
parsed.push_back(text[quotePosition + 1]);
quotePosition += 2;
continue;
}
if (ch == '"') {
outValue = parsed;
if (nextPosition != nullptr) {
*nextPosition = quotePosition + 1;
}
return true;
}
parsed.push_back(ch);
++quotePosition;
}
return false;
}
bool FindValueStart(const std::string& text, const char* key, size_t& outValuePosition) {
const std::string token = std::string("\"") + key + "\"";
const size_t keyPosition = text.find(token);
if (keyPosition == std::string::npos) {
return false;
}
const size_t colonPosition = text.find(':', keyPosition + token.length());
if (colonPosition == std::string::npos) {
return false;
}
outValuePosition = SkipWhitespace(text, colonPosition + 1);
return outValuePosition < text.size();
}
bool TryExtractDelimitedValue(
const std::string& text,
const char* key,
char openChar,
char closeChar,
std::string& outValue) {
size_t valuePosition = 0;
if (!FindValueStart(text, key, valuePosition) || text[valuePosition] != openChar) {
return false;
}
int depth = 0;
bool inString = false;
size_t current = valuePosition;
while (current < text.size()) {
const char ch = text[current];
if (ch == '"' && (current == 0 || text[current - 1] != '\\')) {
inString = !inString;
}
if (!inString) {
if (ch == openChar) {
++depth;
} else if (ch == closeChar) {
--depth;
if (depth == 0) {
outValue = text.substr(valuePosition, current - valuePosition + 1);
return true;
}
}
}
++current;
}
return false;
}
bool TryExtractObject(const std::string& text, const char* key, std::string& outValue) {
return TryExtractDelimitedValue(text, key, '{', '}', outValue);
}
bool TryExtractArray(const std::string& text, const char* key, std::string& outValue) {
return TryExtractDelimitedValue(text, key, '[', ']', outValue);
}
bool CollectObjectKeys(const std::string& objectText, std::unordered_set<std::string>& outKeys) {
if (objectText.empty() || objectText.front() != '{' || objectText.back() != '}') {
return false;
}
size_t position = 1;
while (position < objectText.size()) {
position = SkipWhitespace(objectText, position);
if (position >= objectText.size()) {
return false;
}
if (objectText[position] == '}') {
return true;
}
std::string key;
if (!ParseQuotedString(objectText, position, key, &position)) {
return false;
}
outKeys.insert(key);
position = SkipWhitespace(objectText, position);
if (position >= objectText.size() || objectText[position] != ':') {
return false;
}
position = SkipWhitespace(objectText, position + 1);
if (position >= objectText.size()) {
return false;
}
if (objectText[position] == '"') {
std::string ignoredValue;
if (!ParseQuotedString(objectText, position, ignoredValue, &position)) {
return false;
}
} else if (objectText[position] == '[') {
int depth = 0;
bool inString = false;
do {
const char ch = objectText[position];
if (ch == '"' && (position == 0 || objectText[position - 1] != '\\')) {
inString = !inString;
}
if (!inString) {
if (ch == '[') {
++depth;
} else if (ch == ']') {
--depth;
}
}
++position;
} while (position < objectText.size() && depth > 0);
} else {
while (position < objectText.size() &&
objectText[position] != ',' &&
objectText[position] != '}') {
++position;
}
}
position = SkipWhitespace(objectText, position);
if (position < objectText.size() && objectText[position] == ',') {
++position;
}
}
return false;
}
bool CollectStringArrayValues(const std::string& arrayText, std::unordered_set<std::string>& outValues) {
if (arrayText.empty() || arrayText.front() != '[' || arrayText.back() != ']') {
return false;
}
size_t position = 1;
while (position < arrayText.size()) {
position = SkipWhitespace(arrayText, position);
if (position >= arrayText.size()) {
return false;
}
if (arrayText[position] == ']') {
return true;
}
std::string value;
if (!ParseQuotedString(arrayText, position, value, &position)) {
return false;
}
if (!TrimCopy(value).empty()) {
outValues.insert(TrimCopy(value));
}
position = SkipWhitespace(arrayText, position);
if (position < arrayText.size() && arrayText[position] == ',') {
++position;
}
}
return false;
}
struct MaterialAuthoringPresence {
bool hasRenderStateOverride = false;
std::unordered_set<std::string> keywordValues;
std::unordered_set<std::string> propertyKeys;
std::unordered_set<std::string> textureKeys;
};
MaterialAuthoringPresence ParseMaterialAuthoringPresence(const std::string& text) {
MaterialAuthoringPresence presence;
presence.hasRenderStateOverride = text.find("\"renderState\"") != std::string::npos;
std::string propertiesObject;
if (TryExtractObject(text, "properties", propertiesObject)) {
CollectObjectKeys(propertiesObject, presence.propertyKeys);
}
std::string texturesObject;
if (TryExtractObject(text, "textures", texturesObject)) {
CollectObjectKeys(texturesObject, presence.textureKeys);
}
std::string keywordsArray;
if (TryExtractArray(text, "keywords", keywordsArray)) {
CollectStringArrayValues(keywordsArray, presence.keywordValues);
}
return presence;
}
std::string ReadTextFileOrEmpty(const std::string& path) {
if (path.empty()) {
return std::string();
@@ -704,167 +442,6 @@ const char* SerializeStencilOp(::XCEngine::Resources::MaterialStencilOp op) {
}
}
bool IsTextureMaterialPropertyType(::XCEngine::Resources::MaterialPropertyType type) {
return type == ::XCEngine::Resources::MaterialPropertyType::Texture ||
type == ::XCEngine::Resources::MaterialPropertyType::Cubemap;
}
int GetMaterialPropertyComponentCount(::XCEngine::Resources::MaterialPropertyType type) {
switch (type) {
case ::XCEngine::Resources::MaterialPropertyType::Float2:
case ::XCEngine::Resources::MaterialPropertyType::Int2:
return 2;
case ::XCEngine::Resources::MaterialPropertyType::Float3:
case ::XCEngine::Resources::MaterialPropertyType::Int3:
return 3;
case ::XCEngine::Resources::MaterialPropertyType::Float4:
case ::XCEngine::Resources::MaterialPropertyType::Int4:
return 4;
case ::XCEngine::Resources::MaterialPropertyType::Float:
case ::XCEngine::Resources::MaterialPropertyType::Int:
case ::XCEngine::Resources::MaterialPropertyType::Bool:
case ::XCEngine::Resources::MaterialPropertyType::Texture:
case ::XCEngine::Resources::MaterialPropertyType::Cubemap:
default:
return 1;
}
}
std::string FormatJsonFloat(float value) {
char buffer[32] = {};
std::snprintf(buffer, sizeof(buffer), "%.6g", static_cast<double>(value));
return std::string(buffer);
}
std::string BuildMaterialPropertyValueText(const InspectorPanel::MaterialPropertyState& property) {
using PropertyType = ::XCEngine::Resources::MaterialPropertyType;
switch (property.type) {
case PropertyType::Float:
return FormatJsonFloat(property.floatValue[0]);
case PropertyType::Float2:
case PropertyType::Float3:
case PropertyType::Float4: {
const int componentCount = GetMaterialPropertyComponentCount(property.type);
std::string text = "[";
for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) {
if (componentIndex > 0) {
text += ", ";
}
text += FormatJsonFloat(property.floatValue[componentIndex]);
}
text += "]";
return text;
}
case PropertyType::Int:
return std::to_string(property.intValue[0]);
case PropertyType::Int2:
case PropertyType::Int3:
case PropertyType::Int4: {
const int componentCount = GetMaterialPropertyComponentCount(property.type);
std::string text = "[";
for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) {
if (componentIndex > 0) {
text += ", ";
}
text += std::to_string(property.intValue[componentIndex]);
}
text += "]";
return text;
}
case PropertyType::Bool:
return property.boolValue ? "true" : "false";
case PropertyType::Texture:
case PropertyType::Cubemap:
default:
return std::string();
}
}
bool TryResolveMaterialTextureBindingPath(
const ::XCEngine::Resources::Material& material,
const Containers::String& propertyName,
std::string& outPath) {
for (Core::uint32 bindingIndex = 0; bindingIndex < material.GetTextureBindingCount(); ++bindingIndex) {
if (material.GetTextureBindingName(bindingIndex) != propertyName) {
continue;
}
Containers::String bindingPath = material.GetTextureBindingPath(bindingIndex);
if (bindingPath.Empty()) {
const ::XCEngine::Resources::AssetRef assetRef = material.GetTextureBindingAssetRef(bindingIndex);
if (assetRef.IsValid()) {
::XCEngine::Resources::ResourceManager::Get().TryResolveAssetPath(assetRef, bindingPath);
}
}
outPath = std::string(bindingPath.CStr());
return true;
}
return false;
}
InspectorPanel::MaterialPropertyState BuildMaterialPropertyState(
const ::XCEngine::Resources::MaterialProperty& property,
const ::XCEngine::Resources::Material& material) {
InspectorPanel::MaterialPropertyState state;
state.name = std::string(property.name.CStr());
state.type = property.type;
for (size_t index = 0; index < state.floatValue.size(); ++index) {
state.floatValue[index] = property.value.floatValue[index];
state.intValue[index] = property.value.intValue[index];
}
state.boolValue = property.value.boolValue;
if (IsTextureMaterialPropertyType(property.type)) {
TryResolveMaterialTextureBindingPath(material, property.name, state.texturePath);
}
return state;
}
std::vector<InspectorPanel::MaterialPropertyState> CollectMaterialPropertyStates(
const ::XCEngine::Resources::Material& material) {
std::vector<::XCEngine::Resources::MaterialProperty> properties = material.GetProperties();
const ::XCEngine::Resources::Shader* shader = material.GetShader();
if (shader != nullptr && !shader->GetProperties().Empty()) {
std::unordered_map<std::string, size_t> shaderPropertyOrder;
shaderPropertyOrder.reserve(shader->GetProperties().Size());
for (size_t propertyIndex = 0; propertyIndex < shader->GetProperties().Size(); ++propertyIndex) {
shaderPropertyOrder.emplace(std::string(shader->GetProperties()[propertyIndex].name.CStr()), propertyIndex);
}
std::sort(
properties.begin(),
properties.end(),
[&shaderPropertyOrder](const auto& left, const auto& right) {
const std::string leftName(left.name.CStr());
const std::string rightName(right.name.CStr());
const auto leftIt = shaderPropertyOrder.find(leftName);
const auto rightIt = shaderPropertyOrder.find(rightName);
const size_t leftOrder = leftIt != shaderPropertyOrder.end() ? leftIt->second : shaderPropertyOrder.size();
const size_t rightOrder = rightIt != shaderPropertyOrder.end() ? rightIt->second : shaderPropertyOrder.size();
if (leftOrder != rightOrder) {
return leftOrder < rightOrder;
}
return leftName < rightName;
});
} else {
std::sort(
properties.begin(),
properties.end(),
[](const auto& left, const auto& right) {
return std::strcmp(left.name.CStr(), right.name.CStr()) < 0;
});
}
std::vector<InspectorPanel::MaterialPropertyState> states;
states.reserve(properties.size());
for (const ::XCEngine::Resources::MaterialProperty& property : properties) {
states.push_back(BuildMaterialPropertyState(property, material));
}
return states;
}
void ApplyMaterialKeywordsToResource(
const InspectorPanel::MaterialAssetState& state,
::XCEngine::Resources::Material& material) {
@@ -992,90 +569,6 @@ bool TryResolveShaderHandle(
return outShader.IsValid();
}
void CopyMaterialPropertyValue(
const InspectorPanel::MaterialPropertyState& source,
InspectorPanel::MaterialPropertyState& destination) {
destination.type = source.type;
destination.floatValue = source.floatValue;
destination.intValue = source.intValue;
destination.boolValue = source.boolValue;
destination.texturePath = source.texturePath;
destination.serialized = source.serialized;
}
std::vector<InspectorPanel::MaterialPropertyState> BuildShaderDefaultPropertyStates(
const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle) {
if (!shaderHandle.IsValid()) {
return {};
}
::XCEngine::Resources::Material scratchMaterial;
scratchMaterial.SetShader(shaderHandle);
return CollectMaterialPropertyStates(scratchMaterial);
}
void SyncMaterialAssetStateWithShader(
const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle,
InspectorPanel::MaterialAssetState& state) {
if (!shaderHandle.IsValid() || shaderHandle.Get() == nullptr) {
state.keywords.clear();
state.properties.clear();
return;
}
std::unordered_map<std::string, InspectorPanel::MaterialPropertyState> previousProperties;
previousProperties.reserve(state.properties.size());
for (const InspectorPanel::MaterialPropertyState& property : state.properties) {
if (!property.name.empty()) {
previousProperties.emplace(property.name, property);
}
}
std::vector<InspectorPanel::MaterialPropertyState> nextProperties =
BuildShaderDefaultPropertyStates(shaderHandle);
for (InspectorPanel::MaterialPropertyState& property : nextProperties) {
const auto previousPropertyIt = previousProperties.find(property.name);
if (previousPropertyIt == previousProperties.end()) {
continue;
}
if (previousPropertyIt->second.type != property.type) {
continue;
}
CopyMaterialPropertyValue(previousPropertyIt->second, property);
}
state.properties = std::move(nextProperties);
std::vector<InspectorPanel::MaterialKeywordState> nextKeywords;
nextKeywords.reserve(state.keywords.size());
for (const InspectorPanel::MaterialKeywordState& keyword : state.keywords) {
const std::string keywordValue = TrimCopy(keyword.value);
if (!keywordValue.empty() && shaderHandle->DeclaresKeyword(keywordValue.c_str())) {
nextKeywords.push_back(keyword);
}
}
state.keywords = std::move(nextKeywords);
}
void ApplyMaterialAuthoringPresenceToState(
const MaterialAuthoringPresence& presence,
InspectorPanel::MaterialAssetState& state) {
state.hasRenderStateOverride = presence.hasRenderStateOverride;
for (InspectorPanel::MaterialKeywordState& keyword : state.keywords) {
keyword.serialized = presence.keywordValues.find(TrimCopy(keyword.value)) != presence.keywordValues.end();
}
for (InspectorPanel::MaterialPropertyState& property : state.properties) {
if (IsTextureMaterialPropertyType(property.type)) {
property.serialized = presence.textureKeys.find(property.name) != presence.textureKeys.end();
} else {
property.serialized = presence.propertyKeys.find(property.name) != presence.propertyKeys.end();
}
}
}
InspectorPanel::MaterialPropertyState* FindMaterialPropertyState(
InspectorPanel::MaterialAssetState& state,
const Containers::String& propertyName) {
@@ -1223,212 +716,6 @@ std::string BuildShaderLoadFailureMessage(
return message;
}
std::string BuildMaterialAssetFileText(const InspectorPanel::MaterialAssetState& state) {
std::vector<std::string> rootEntries;
const std::string shaderPath = TrimCopy(BufferToString(state.shaderPath));
if (!shaderPath.empty()) {
rootEntries.push_back(" \"shader\": \"" + EscapeJsonString(shaderPath) + "\"");
}
const int renderQueuePreset = ResolveRenderQueuePresetIndex(state.renderQueue);
if (renderQueuePreset != kCustomRenderQueuePresetIndex) {
rootEntries.push_back(
" \"renderQueue\": \"" + std::string(SerializeRenderQueue(state.renderQueue)) + "\"");
} else {
rootEntries.push_back(" \"renderQueue\": " + std::to_string(state.renderQueue));
}
std::vector<std::string> tagEntries;
for (const InspectorPanel::MaterialTagEditRow& row : state.tags) {
const std::string tagName = TrimCopy(BufferToString(row.name));
if (tagName.empty()) {
continue;
}
tagEntries.push_back(
" \"" + EscapeJsonString(tagName) + "\": \"" +
EscapeJsonString(TrimCopy(BufferToString(row.value))) + "\"");
}
if (!tagEntries.empty()) {
std::string tagsObject = " \"tags\": {\n";
for (size_t tagIndex = 0; tagIndex < tagEntries.size(); ++tagIndex) {
tagsObject += tagEntries[tagIndex];
if (tagIndex + 1 < tagEntries.size()) {
tagsObject += ",";
}
tagsObject += "\n";
}
tagsObject += " }";
rootEntries.push_back(tagsObject);
}
if (!state.keywords.empty()) {
std::string keywordsArray = " \"keywords\": [";
bool firstKeyword = true;
for (const InspectorPanel::MaterialKeywordState& keyword : state.keywords) {
if (!keyword.serialized) {
continue;
}
const std::string keywordValue = TrimCopy(keyword.value);
if (keywordValue.empty()) {
continue;
}
if (!firstKeyword) {
keywordsArray += ", ";
}
keywordsArray += "\"" + EscapeJsonString(keywordValue) + "\"";
firstKeyword = false;
}
keywordsArray += "]";
if (!firstKeyword) {
rootEntries.push_back(keywordsArray);
}
}
std::vector<std::string> propertyEntries;
std::vector<std::string> textureEntries;
for (const InspectorPanel::MaterialPropertyState& property : state.properties) {
if (property.name.empty()) {
continue;
}
if (IsTextureMaterialPropertyType(property.type)) {
if (!property.serialized) {
continue;
}
const std::string texturePath = TrimCopy(property.texturePath);
if (!texturePath.empty()) {
textureEntries.push_back(
" \"" + EscapeJsonString(property.name) + "\": \"" +
EscapeJsonString(texturePath) + "\"");
}
continue;
}
if (!property.serialized) {
continue;
}
const std::string propertyValueText = BuildMaterialPropertyValueText(property);
if (!propertyValueText.empty()) {
propertyEntries.push_back(
" \"" + EscapeJsonString(property.name) + "\": " + propertyValueText);
}
}
if (!propertyEntries.empty()) {
std::string propertiesObject = " \"properties\": {\n";
for (size_t propertyIndex = 0; propertyIndex < propertyEntries.size(); ++propertyIndex) {
propertiesObject += propertyEntries[propertyIndex];
if (propertyIndex + 1 < propertyEntries.size()) {
propertiesObject += ",";
}
propertiesObject += "\n";
}
propertiesObject += " }";
rootEntries.push_back(propertiesObject);
}
if (!textureEntries.empty()) {
std::string texturesObject = " \"textures\": {\n";
for (size_t textureIndex = 0; textureIndex < textureEntries.size(); ++textureIndex) {
texturesObject += textureEntries[textureIndex];
if (textureIndex + 1 < textureEntries.size()) {
texturesObject += ",";
}
texturesObject += "\n";
}
texturesObject += " }";
rootEntries.push_back(texturesObject);
}
if (state.hasRenderStateOverride) {
const ::XCEngine::Resources::MaterialRenderState& renderState = state.renderState;
std::vector<std::string> renderStateEntries;
renderStateEntries.push_back(
" \"cull\": \"" + std::string(SerializeCullMode(renderState.cullMode)) + "\"");
renderStateEntries.push_back(std::string(" \"depthTest\": ") + (renderState.depthTestEnable ? "true" : "false"));
renderStateEntries.push_back(std::string(" \"depthWrite\": ") + (renderState.depthWriteEnable ? "true" : "false"));
renderStateEntries.push_back(
" \"depthFunc\": \"" + std::string(SerializeComparisonFunc(renderState.depthFunc)) + "\"");
renderStateEntries.push_back(std::string(" \"blendEnable\": ") + (renderState.blendEnable ? "true" : "false"));
renderStateEntries.push_back(
" \"srcBlend\": \"" + std::string(SerializeBlendFactor(renderState.srcBlend)) + "\"");
renderStateEntries.push_back(
" \"dstBlend\": \"" + std::string(SerializeBlendFactor(renderState.dstBlend)) + "\"");
renderStateEntries.push_back(
" \"srcBlendAlpha\": \"" + std::string(SerializeBlendFactor(renderState.srcBlendAlpha)) + "\"");
renderStateEntries.push_back(
" \"dstBlendAlpha\": \"" + std::string(SerializeBlendFactor(renderState.dstBlendAlpha)) + "\"");
renderStateEntries.push_back(
" \"blendOp\": \"" + std::string(SerializeBlendOp(renderState.blendOp)) + "\"");
renderStateEntries.push_back(
" \"blendOpAlpha\": \"" + std::string(SerializeBlendOp(renderState.blendOpAlpha)) + "\"");
renderStateEntries.push_back(" \"colorWriteMask\": " + std::to_string(renderState.colorWriteMask));
if (renderState.depthBiasFactor != 0.0f || renderState.depthBiasUnits != 0) {
renderStateEntries.push_back(
" \"offset\": [" +
FormatJsonFloat(renderState.depthBiasFactor) + ", " +
std::to_string(renderState.depthBiasUnits) + "]");
}
if (renderState.stencil.enabled) {
std::vector<std::string> stencilEntries;
stencilEntries.push_back(" \"enabled\": true");
stencilEntries.push_back(" \"ref\": " + std::to_string(renderState.stencil.reference));
stencilEntries.push_back(" \"readMask\": " + std::to_string(renderState.stencil.readMask));
stencilEntries.push_back(" \"writeMask\": " + std::to_string(renderState.stencil.writeMask));
stencilEntries.push_back(
" \"comp\": \"" + std::string(SerializeComparisonFunc(renderState.stencil.front.func)) + "\"");
stencilEntries.push_back(
" \"pass\": \"" + std::string(SerializeStencilOp(renderState.stencil.front.passOp)) + "\"");
stencilEntries.push_back(
" \"fail\": \"" + std::string(SerializeStencilOp(renderState.stencil.front.failOp)) + "\"");
stencilEntries.push_back(
" \"zFail\": \"" + std::string(SerializeStencilOp(renderState.stencil.front.depthFailOp)) + "\"");
stencilEntries.push_back(
" \"compBack\": \"" + std::string(SerializeComparisonFunc(renderState.stencil.back.func)) + "\"");
stencilEntries.push_back(
" \"passBack\": \"" + std::string(SerializeStencilOp(renderState.stencil.back.passOp)) + "\"");
stencilEntries.push_back(
" \"failBack\": \"" + std::string(SerializeStencilOp(renderState.stencil.back.failOp)) + "\"");
stencilEntries.push_back(
" \"zFailBack\": \"" + std::string(SerializeStencilOp(renderState.stencil.back.depthFailOp)) + "\"");
std::string stencilObject = " \"stencil\": {\n";
for (size_t stencilIndex = 0; stencilIndex < stencilEntries.size(); ++stencilIndex) {
stencilObject += stencilEntries[stencilIndex];
if (stencilIndex + 1 < stencilEntries.size()) {
stencilObject += ",";
}
stencilObject += "\n";
}
stencilObject += " }";
renderStateEntries.push_back(stencilObject);
}
std::string renderStateObject = " \"renderState\": {\n";
for (size_t renderStateIndex = 0; renderStateIndex < renderStateEntries.size(); ++renderStateIndex) {
renderStateObject += renderStateEntries[renderStateIndex];
if (renderStateIndex + 1 < renderStateEntries.size()) {
renderStateObject += ",";
}
renderStateObject += "\n";
}
renderStateObject += " }";
rootEntries.push_back(renderStateObject);
}
std::string json = "{\n";
for (size_t entryIndex = 0; entryIndex < rootEntries.size(); ++entryIndex) {
json += rootEntries[entryIndex];
if (entryIndex + 1 < rootEntries.size()) {
json += ",";
}
json += "\n";
}
json += "}\n";
return json;
}
} // namespace
InspectorPanel::InspectorPanel() : Panel("Inspector") {}
@@ -1525,9 +812,7 @@ void InspectorPanel::PopulateMaterialAssetStateFromResource(::XCEngine::Resource
const std::string sourceText = ReadTextFileOrEmpty(m_materialAssetState.assetFullPath);
if (!sourceText.empty()) {
ApplyMaterialAuthoringPresenceToState(
ParseMaterialAuthoringPresence(sourceText),
m_materialAssetState);
ApplyMaterialAuthoringPresenceToState(sourceText, m_materialAssetState);
}
m_materialAssetState.loaded = true;

View File

@@ -1,5 +1,6 @@
#pragma once
#include "MaterialInspectorMaterialState.h"
#include "Panel.h"
#include "Core/AssetItem.h"
#include "Core/EditorActionRoute.h"
@@ -38,58 +39,10 @@ public:
UnsupportedAsset
};
struct MaterialTagEditRow {
std::array<char, 64> name{};
std::array<char, 128> value{};
};
struct MaterialKeywordState {
std::string value;
bool serialized = false;
};
struct MaterialPropertyState {
std::string name;
::XCEngine::Resources::MaterialPropertyType type =
::XCEngine::Resources::MaterialPropertyType::Float;
std::array<float, 4> floatValue{};
std::array<::XCEngine::Core::int32, 4> intValue{};
bool boolValue = false;
std::string texturePath;
bool serialized = false;
};
struct MaterialAssetState {
std::string assetPath;
std::string assetFullPath;
std::string assetName;
std::array<char, 260> shaderPath{};
int renderQueue = static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Geometry);
::XCEngine::Resources::MaterialRenderState renderState{};
bool hasRenderStateOverride = false;
std::vector<MaterialTagEditRow> tags;
std::vector<MaterialKeywordState> keywords;
std::vector<MaterialPropertyState> properties;
std::string errorMessage;
bool dirty = false;
bool loaded = false;
void Reset() {
assetPath.clear();
assetFullPath.clear();
assetName.clear();
shaderPath.fill('\0');
renderQueue = static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Geometry);
renderState = ::XCEngine::Resources::MaterialRenderState();
hasRenderStateOverride = false;
tags.clear();
keywords.clear();
properties.clear();
errorMessage.clear();
dirty = false;
loaded = false;
}
};
using MaterialTagEditRow = ::XCEngine::Editor::MaterialTagEditRow;
using MaterialKeywordState = ::XCEngine::Editor::MaterialKeywordState;
using MaterialPropertyState = ::XCEngine::Editor::MaterialPropertyState;
using MaterialAssetState = ::XCEngine::Editor::MaterialAssetState;
private:
void SyncSubject();

View File

@@ -0,0 +1,66 @@
#pragma once
#include <XCEngine/Resources/Material/Material.h>
#include <array>
#include <string>
#include <vector>
namespace XCEngine {
namespace Editor {
struct MaterialTagEditRow {
std::array<char, 64> name{};
std::array<char, 128> value{};
};
struct MaterialKeywordState {
std::string value;
bool serialized = false;
};
struct MaterialPropertyState {
std::string name;
::XCEngine::Resources::MaterialPropertyType type =
::XCEngine::Resources::MaterialPropertyType::Float;
std::array<float, 4> floatValue{};
std::array<::XCEngine::Core::int32, 4> intValue{};
bool boolValue = false;
std::string texturePath;
bool serialized = false;
};
struct MaterialAssetState {
std::string assetPath;
std::string assetFullPath;
std::string assetName;
std::array<char, 260> shaderPath{};
int renderQueue = static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Geometry);
::XCEngine::Resources::MaterialRenderState renderState{};
bool hasRenderStateOverride = false;
std::vector<MaterialTagEditRow> tags;
std::vector<MaterialKeywordState> keywords;
std::vector<MaterialPropertyState> properties;
std::string errorMessage;
bool dirty = false;
bool loaded = false;
void Reset() {
assetPath.clear();
assetFullPath.clear();
assetName.clear();
shaderPath.fill('\0');
renderQueue = static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Geometry);
renderState = ::XCEngine::Resources::MaterialRenderState();
hasRenderStateOverride = false;
tags.clear();
keywords.clear();
properties.clear();
errorMessage.clear();
dirty = false;
loaded = false;
}
};
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,893 @@
#include "MaterialInspectorMaterialStateIO.h"
#include "Utils/ProjectFileUtils.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Resources/Shader/Shader.h>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <string>
#include <unordered_map>
#include <unordered_set>
namespace XCEngine {
namespace Editor {
namespace {
template <size_t N>
std::string BufferToString(const std::array<char, N>& buffer) {
return std::string(buffer.data());
}
std::string TrimCopy(const std::string& value) {
return ProjectFileUtils::Trim(value);
}
size_t SkipWhitespace(const std::string& text, size_t position) {
while (position < text.size() && std::isspace(static_cast<unsigned char>(text[position])) != 0) {
++position;
}
return position;
}
bool ParseQuotedString(
const std::string& text,
size_t quotePosition,
std::string& outValue,
size_t* nextPosition = nullptr) {
if (quotePosition >= text.size() || text[quotePosition] != '"') {
return false;
}
std::string parsed;
++quotePosition;
while (quotePosition < text.size()) {
const char ch = text[quotePosition];
if (ch == '\\') {
if (quotePosition + 1 >= text.size()) {
return false;
}
parsed.push_back(text[quotePosition + 1]);
quotePosition += 2;
continue;
}
if (ch == '"') {
outValue = parsed;
if (nextPosition != nullptr) {
*nextPosition = quotePosition + 1;
}
return true;
}
parsed.push_back(ch);
++quotePosition;
}
return false;
}
bool FindValueStart(const std::string& text, const char* key, size_t& outValuePosition) {
const std::string token = std::string("\"") + key + "\"";
const size_t keyPosition = text.find(token);
if (keyPosition == std::string::npos) {
return false;
}
const size_t colonPosition = text.find(':', keyPosition + token.length());
if (colonPosition == std::string::npos) {
return false;
}
outValuePosition = SkipWhitespace(text, colonPosition + 1);
return outValuePosition < text.size();
}
bool TryExtractDelimitedValue(
const std::string& text,
const char* key,
char openChar,
char closeChar,
std::string& outValue) {
size_t valuePosition = 0;
if (!FindValueStart(text, key, valuePosition) || text[valuePosition] != openChar) {
return false;
}
int depth = 0;
bool inString = false;
size_t current = valuePosition;
while (current < text.size()) {
const char ch = text[current];
if (ch == '"' && (current == 0 || text[current - 1] != '\\')) {
inString = !inString;
}
if (!inString) {
if (ch == openChar) {
++depth;
} else if (ch == closeChar) {
--depth;
if (depth == 0) {
outValue = text.substr(valuePosition, current - valuePosition + 1);
return true;
}
}
}
++current;
}
return false;
}
bool TryExtractObject(const std::string& text, const char* key, std::string& outValue) {
return TryExtractDelimitedValue(text, key, '{', '}', outValue);
}
bool TryExtractArray(const std::string& text, const char* key, std::string& outValue) {
return TryExtractDelimitedValue(text, key, '[', ']', outValue);
}
bool CollectObjectKeys(const std::string& objectText, std::unordered_set<std::string>& outKeys) {
if (objectText.empty() || objectText.front() != '{' || objectText.back() != '}') {
return false;
}
size_t position = 1;
while (position < objectText.size()) {
position = SkipWhitespace(objectText, position);
if (position >= objectText.size()) {
return false;
}
if (objectText[position] == '}') {
return true;
}
std::string key;
if (!ParseQuotedString(objectText, position, key, &position)) {
return false;
}
outKeys.insert(key);
position = SkipWhitespace(objectText, position);
if (position >= objectText.size() || objectText[position] != ':') {
return false;
}
position = SkipWhitespace(objectText, position + 1);
if (position >= objectText.size()) {
return false;
}
if (objectText[position] == '"') {
std::string ignoredValue;
if (!ParseQuotedString(objectText, position, ignoredValue, &position)) {
return false;
}
} else if (objectText[position] == '[') {
int depth = 0;
bool inString = false;
do {
const char ch = objectText[position];
if (ch == '"' && (position == 0 || objectText[position - 1] != '\\')) {
inString = !inString;
}
if (!inString) {
if (ch == '[') {
++depth;
} else if (ch == ']') {
--depth;
}
}
++position;
} while (position < objectText.size() && depth > 0);
} else {
while (position < objectText.size() &&
objectText[position] != ',' &&
objectText[position] != '}') {
++position;
}
}
position = SkipWhitespace(objectText, position);
if (position < objectText.size() && objectText[position] == ',') {
++position;
}
}
return false;
}
bool CollectStringArrayValues(const std::string& arrayText, std::unordered_set<std::string>& outValues) {
if (arrayText.empty() || arrayText.front() != '[' || arrayText.back() != ']') {
return false;
}
size_t position = 1;
while (position < arrayText.size()) {
position = SkipWhitespace(arrayText, position);
if (position >= arrayText.size()) {
return false;
}
if (arrayText[position] == ']') {
return true;
}
std::string value;
if (!ParseQuotedString(arrayText, position, value, &position)) {
return false;
}
if (!TrimCopy(value).empty()) {
outValues.insert(TrimCopy(value));
}
position = SkipWhitespace(arrayText, position);
if (position < arrayText.size() && arrayText[position] == ',') {
++position;
}
}
return false;
}
struct MaterialAuthoringPresence {
bool hasRenderStateOverride = false;
std::unordered_set<std::string> keywordValues;
std::unordered_set<std::string> propertyKeys;
std::unordered_set<std::string> textureKeys;
};
MaterialAuthoringPresence ParseMaterialAuthoringPresence(const std::string& text) {
MaterialAuthoringPresence presence;
presence.hasRenderStateOverride = text.find("\"renderState\"") != std::string::npos;
std::string propertiesObject;
if (TryExtractObject(text, "properties", propertiesObject)) {
CollectObjectKeys(propertiesObject, presence.propertyKeys);
}
std::string texturesObject;
if (TryExtractObject(text, "textures", texturesObject)) {
CollectObjectKeys(texturesObject, presence.textureKeys);
}
std::string keywordsArray;
if (TryExtractArray(text, "keywords", keywordsArray)) {
CollectStringArrayValues(keywordsArray, presence.keywordValues);
}
return presence;
}
int GetMaterialPropertyComponentCount(::XCEngine::Resources::MaterialPropertyType type) {
switch (type) {
case ::XCEngine::Resources::MaterialPropertyType::Float2:
case ::XCEngine::Resources::MaterialPropertyType::Int2:
return 2;
case ::XCEngine::Resources::MaterialPropertyType::Float3:
case ::XCEngine::Resources::MaterialPropertyType::Int3:
return 3;
case ::XCEngine::Resources::MaterialPropertyType::Float4:
case ::XCEngine::Resources::MaterialPropertyType::Int4:
return 4;
case ::XCEngine::Resources::MaterialPropertyType::Float:
case ::XCEngine::Resources::MaterialPropertyType::Int:
case ::XCEngine::Resources::MaterialPropertyType::Bool:
case ::XCEngine::Resources::MaterialPropertyType::Texture:
case ::XCEngine::Resources::MaterialPropertyType::Cubemap:
default:
return 1;
}
}
std::string FormatJsonFloat(float value) {
char buffer[32] = {};
std::snprintf(buffer, sizeof(buffer), "%.6g", static_cast<double>(value));
return std::string(buffer);
}
std::string EscapeJsonString(const std::string& value) {
std::string escaped;
escaped.reserve(value.size());
for (const char ch : value) {
switch (ch) {
case '\\':
escaped += "\\\\";
break;
case '"':
escaped += "\\\"";
break;
case '\n':
escaped += "\\n";
break;
case '\r':
escaped += "\\r";
break;
case '\t':
escaped += "\\t";
break;
default:
escaped.push_back(ch);
break;
}
}
return escaped;
}
std::string BuildMaterialPropertyValueText(const MaterialPropertyState& property) {
using PropertyType = ::XCEngine::Resources::MaterialPropertyType;
switch (property.type) {
case PropertyType::Float:
return FormatJsonFloat(property.floatValue[0]);
case PropertyType::Float2:
case PropertyType::Float3:
case PropertyType::Float4: {
const int componentCount = GetMaterialPropertyComponentCount(property.type);
std::string text = "[";
for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) {
if (componentIndex > 0) {
text += ", ";
}
text += FormatJsonFloat(property.floatValue[componentIndex]);
}
text += "]";
return text;
}
case PropertyType::Int:
return std::to_string(property.intValue[0]);
case PropertyType::Int2:
case PropertyType::Int3:
case PropertyType::Int4: {
const int componentCount = GetMaterialPropertyComponentCount(property.type);
std::string text = "[";
for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) {
if (componentIndex > 0) {
text += ", ";
}
text += std::to_string(property.intValue[componentIndex]);
}
text += "]";
return text;
}
case PropertyType::Bool:
return property.boolValue ? "true" : "false";
case PropertyType::Texture:
case PropertyType::Cubemap:
default:
return std::string();
}
}
bool TryResolveMaterialTextureBindingPath(
const ::XCEngine::Resources::Material& material,
const Containers::String& propertyName,
std::string& outPath) {
for (Core::uint32 bindingIndex = 0; bindingIndex < material.GetTextureBindingCount(); ++bindingIndex) {
if (material.GetTextureBindingName(bindingIndex) != propertyName) {
continue;
}
Containers::String bindingPath = material.GetTextureBindingPath(bindingIndex);
if (bindingPath.Empty()) {
const ::XCEngine::Resources::AssetRef assetRef = material.GetTextureBindingAssetRef(bindingIndex);
if (assetRef.IsValid()) {
::XCEngine::Resources::ResourceManager::Get().TryResolveAssetPath(assetRef, bindingPath);
}
}
outPath = std::string(bindingPath.CStr());
return true;
}
return false;
}
MaterialPropertyState BuildMaterialPropertyState(
const ::XCEngine::Resources::MaterialProperty& property,
const ::XCEngine::Resources::Material& material) {
MaterialPropertyState state;
state.name = std::string(property.name.CStr());
state.type = property.type;
for (size_t index = 0; index < state.floatValue.size(); ++index) {
state.floatValue[index] = property.value.floatValue[index];
state.intValue[index] = property.value.intValue[index];
}
state.boolValue = property.value.boolValue;
if (IsTextureMaterialPropertyType(property.type)) {
TryResolveMaterialTextureBindingPath(material, property.name, state.texturePath);
}
return state;
}
void CopyMaterialPropertyValue(
const MaterialPropertyState& source,
MaterialPropertyState& destination) {
destination.type = source.type;
destination.floatValue = source.floatValue;
destination.intValue = source.intValue;
destination.boolValue = source.boolValue;
destination.texturePath = source.texturePath;
destination.serialized = source.serialized;
}
std::vector<MaterialPropertyState> BuildShaderDefaultPropertyStates(
const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle) {
if (!shaderHandle.IsValid()) {
return {};
}
::XCEngine::Resources::Material scratchMaterial;
scratchMaterial.SetShader(shaderHandle);
return CollectMaterialPropertyStates(scratchMaterial);
}
constexpr int kCustomRenderQueuePresetIndex = 5;
int ResolveRenderQueuePresetIndex(int renderQueue) {
switch (renderQueue) {
case static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Background):
return 0;
case static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Geometry):
return 1;
case static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::AlphaTest):
return 2;
case static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Transparent):
return 3;
case static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Overlay):
return 4;
default:
return kCustomRenderQueuePresetIndex;
}
}
const char* SerializeRenderQueue(int renderQueue) {
switch (renderQueue) {
case static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Background):
return "background";
case static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Geometry):
return "geometry";
case static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::AlphaTest):
return "alpha_test";
case static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Transparent):
return "transparent";
case static_cast<int>(::XCEngine::Resources::MaterialRenderQueue::Overlay):
return "overlay";
default:
return nullptr;
}
}
const char* SerializeCullMode(::XCEngine::Resources::MaterialCullMode mode) {
switch (mode) {
case ::XCEngine::Resources::MaterialCullMode::Front:
return "front";
case ::XCEngine::Resources::MaterialCullMode::Back:
return "back";
case ::XCEngine::Resources::MaterialCullMode::None:
default:
return "none";
}
}
const char* SerializeComparisonFunc(::XCEngine::Resources::MaterialComparisonFunc func) {
switch (func) {
case ::XCEngine::Resources::MaterialComparisonFunc::Never:
return "never";
case ::XCEngine::Resources::MaterialComparisonFunc::Less:
return "less";
case ::XCEngine::Resources::MaterialComparisonFunc::Equal:
return "equal";
case ::XCEngine::Resources::MaterialComparisonFunc::LessEqual:
return "less_equal";
case ::XCEngine::Resources::MaterialComparisonFunc::Greater:
return "greater";
case ::XCEngine::Resources::MaterialComparisonFunc::NotEqual:
return "not_equal";
case ::XCEngine::Resources::MaterialComparisonFunc::GreaterEqual:
return "greater_equal";
case ::XCEngine::Resources::MaterialComparisonFunc::Always:
default:
return "always";
}
}
const char* SerializeBlendOp(::XCEngine::Resources::MaterialBlendOp op) {
switch (op) {
case ::XCEngine::Resources::MaterialBlendOp::Subtract:
return "subtract";
case ::XCEngine::Resources::MaterialBlendOp::ReverseSubtract:
return "reverse_subtract";
case ::XCEngine::Resources::MaterialBlendOp::Min:
return "min";
case ::XCEngine::Resources::MaterialBlendOp::Max:
return "max";
case ::XCEngine::Resources::MaterialBlendOp::Add:
default:
return "add";
}
}
const char* SerializeBlendFactor(::XCEngine::Resources::MaterialBlendFactor factor) {
switch (factor) {
case ::XCEngine::Resources::MaterialBlendFactor::Zero:
return "zero";
case ::XCEngine::Resources::MaterialBlendFactor::One:
return "one";
case ::XCEngine::Resources::MaterialBlendFactor::SrcColor:
return "src_color";
case ::XCEngine::Resources::MaterialBlendFactor::InvSrcColor:
return "one_minus_src_color";
case ::XCEngine::Resources::MaterialBlendFactor::SrcAlpha:
return "src_alpha";
case ::XCEngine::Resources::MaterialBlendFactor::InvSrcAlpha:
return "one_minus_src_alpha";
case ::XCEngine::Resources::MaterialBlendFactor::DstAlpha:
return "dst_alpha";
case ::XCEngine::Resources::MaterialBlendFactor::InvDstAlpha:
return "one_minus_dst_alpha";
case ::XCEngine::Resources::MaterialBlendFactor::DstColor:
return "dst_color";
case ::XCEngine::Resources::MaterialBlendFactor::InvDstColor:
return "one_minus_dst_color";
case ::XCEngine::Resources::MaterialBlendFactor::SrcAlphaSat:
return "src_alpha_sat";
case ::XCEngine::Resources::MaterialBlendFactor::BlendFactor:
return "blend_factor";
case ::XCEngine::Resources::MaterialBlendFactor::InvBlendFactor:
return "one_minus_blend_factor";
case ::XCEngine::Resources::MaterialBlendFactor::Src1Color:
return "src1_color";
case ::XCEngine::Resources::MaterialBlendFactor::InvSrc1Color:
return "one_minus_src1_color";
case ::XCEngine::Resources::MaterialBlendFactor::Src1Alpha:
return "src1_alpha";
case ::XCEngine::Resources::MaterialBlendFactor::InvSrc1Alpha:
default:
return "one_minus_src1_alpha";
}
}
const char* SerializeStencilOp(::XCEngine::Resources::MaterialStencilOp op) {
switch (op) {
case ::XCEngine::Resources::MaterialStencilOp::Zero:
return "zero";
case ::XCEngine::Resources::MaterialStencilOp::Replace:
return "replace";
case ::XCEngine::Resources::MaterialStencilOp::IncrSat:
return "incrsat";
case ::XCEngine::Resources::MaterialStencilOp::DecrSat:
return "decrsat";
case ::XCEngine::Resources::MaterialStencilOp::Invert:
return "invert";
case ::XCEngine::Resources::MaterialStencilOp::IncrWrap:
return "incrwrap";
case ::XCEngine::Resources::MaterialStencilOp::DecrWrap:
return "decrwrap";
case ::XCEngine::Resources::MaterialStencilOp::Keep:
default:
return "keep";
}
}
} // namespace
bool IsTextureMaterialPropertyType(::XCEngine::Resources::MaterialPropertyType type) {
return type == ::XCEngine::Resources::MaterialPropertyType::Texture ||
type == ::XCEngine::Resources::MaterialPropertyType::Cubemap;
}
std::vector<MaterialPropertyState> CollectMaterialPropertyStates(
const ::XCEngine::Resources::Material& material) {
std::vector<::XCEngine::Resources::MaterialProperty> properties = material.GetProperties();
const ::XCEngine::Resources::Shader* shader = material.GetShader();
if (shader != nullptr && !shader->GetProperties().Empty()) {
std::unordered_map<std::string, size_t> shaderPropertyOrder;
shaderPropertyOrder.reserve(shader->GetProperties().Size());
for (size_t propertyIndex = 0; propertyIndex < shader->GetProperties().Size(); ++propertyIndex) {
shaderPropertyOrder.emplace(std::string(shader->GetProperties()[propertyIndex].name.CStr()), propertyIndex);
}
std::sort(
properties.begin(),
properties.end(),
[&shaderPropertyOrder](const auto& left, const auto& right) {
const std::string leftName(left.name.CStr());
const std::string rightName(right.name.CStr());
const auto leftIt = shaderPropertyOrder.find(leftName);
const auto rightIt = shaderPropertyOrder.find(rightName);
const size_t leftOrder = leftIt != shaderPropertyOrder.end() ? leftIt->second : shaderPropertyOrder.size();
const size_t rightOrder = rightIt != shaderPropertyOrder.end() ? rightIt->second : shaderPropertyOrder.size();
if (leftOrder != rightOrder) {
return leftOrder < rightOrder;
}
return leftName < rightName;
});
} else {
std::sort(
properties.begin(),
properties.end(),
[](const auto& left, const auto& right) {
return std::strcmp(left.name.CStr(), right.name.CStr()) < 0;
});
}
std::vector<MaterialPropertyState> states;
states.reserve(properties.size());
for (const ::XCEngine::Resources::MaterialProperty& property : properties) {
states.push_back(BuildMaterialPropertyState(property, material));
}
return states;
}
void SyncMaterialAssetStateWithShader(
const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle,
MaterialAssetState& state) {
if (!shaderHandle.IsValid() || shaderHandle.Get() == nullptr) {
state.keywords.clear();
state.properties.clear();
return;
}
std::unordered_map<std::string, MaterialPropertyState> previousProperties;
previousProperties.reserve(state.properties.size());
for (const MaterialPropertyState& property : state.properties) {
if (!property.name.empty()) {
previousProperties.emplace(property.name, property);
}
}
std::vector<MaterialPropertyState> nextProperties = BuildShaderDefaultPropertyStates(shaderHandle);
for (MaterialPropertyState& property : nextProperties) {
const auto previousPropertyIt = previousProperties.find(property.name);
if (previousPropertyIt == previousProperties.end()) {
continue;
}
if (previousPropertyIt->second.type != property.type) {
continue;
}
CopyMaterialPropertyValue(previousPropertyIt->second, property);
}
state.properties = std::move(nextProperties);
std::vector<MaterialKeywordState> nextKeywords;
nextKeywords.reserve(state.keywords.size());
for (const MaterialKeywordState& keyword : state.keywords) {
const std::string keywordValue = TrimCopy(keyword.value);
if (!keywordValue.empty() && shaderHandle->DeclaresKeyword(keywordValue.c_str())) {
nextKeywords.push_back(keyword);
}
}
state.keywords = std::move(nextKeywords);
}
void ApplyMaterialAuthoringPresenceToState(
const std::string& sourceText,
MaterialAssetState& state) {
const MaterialAuthoringPresence presence = ParseMaterialAuthoringPresence(sourceText);
state.hasRenderStateOverride = presence.hasRenderStateOverride;
for (MaterialKeywordState& keyword : state.keywords) {
keyword.serialized = presence.keywordValues.find(TrimCopy(keyword.value)) != presence.keywordValues.end();
}
for (MaterialPropertyState& property : state.properties) {
if (IsTextureMaterialPropertyType(property.type)) {
property.serialized = presence.textureKeys.find(property.name) != presence.textureKeys.end();
} else {
property.serialized = presence.propertyKeys.find(property.name) != presence.propertyKeys.end();
}
}
}
std::string BuildMaterialAssetFileText(const MaterialAssetState& state) {
std::vector<std::string> rootEntries;
const std::string shaderPath = TrimCopy(BufferToString(state.shaderPath));
if (!shaderPath.empty()) {
rootEntries.push_back(" \"shader\": \"" + EscapeJsonString(shaderPath) + "\"");
}
const int renderQueuePreset = ResolveRenderQueuePresetIndex(state.renderQueue);
if (renderQueuePreset != kCustomRenderQueuePresetIndex) {
rootEntries.push_back(
" \"renderQueue\": \"" + std::string(SerializeRenderQueue(state.renderQueue)) + "\"");
} else {
rootEntries.push_back(" \"renderQueue\": " + std::to_string(state.renderQueue));
}
std::vector<std::string> tagEntries;
for (const MaterialTagEditRow& row : state.tags) {
const std::string tagName = TrimCopy(BufferToString(row.name));
if (tagName.empty()) {
continue;
}
tagEntries.push_back(
" \"" + EscapeJsonString(tagName) + "\": \"" +
EscapeJsonString(TrimCopy(BufferToString(row.value))) + "\"");
}
if (!tagEntries.empty()) {
std::string tagsObject = " \"tags\": {\n";
for (size_t tagIndex = 0; tagIndex < tagEntries.size(); ++tagIndex) {
tagsObject += tagEntries[tagIndex];
if (tagIndex + 1 < tagEntries.size()) {
tagsObject += ",";
}
tagsObject += "\n";
}
tagsObject += " }";
rootEntries.push_back(tagsObject);
}
if (!state.keywords.empty()) {
std::string keywordsArray = " \"keywords\": [";
bool firstKeyword = true;
for (const MaterialKeywordState& keyword : state.keywords) {
if (!keyword.serialized) {
continue;
}
const std::string keywordValue = TrimCopy(keyword.value);
if (keywordValue.empty()) {
continue;
}
if (!firstKeyword) {
keywordsArray += ", ";
}
keywordsArray += "\"" + EscapeJsonString(keywordValue) + "\"";
firstKeyword = false;
}
keywordsArray += "]";
if (!firstKeyword) {
rootEntries.push_back(keywordsArray);
}
}
std::vector<std::string> propertyEntries;
std::vector<std::string> textureEntries;
for (const MaterialPropertyState& property : state.properties) {
if (property.name.empty()) {
continue;
}
if (IsTextureMaterialPropertyType(property.type)) {
if (!property.serialized) {
continue;
}
const std::string texturePath = TrimCopy(property.texturePath);
if (!texturePath.empty()) {
textureEntries.push_back(
" \"" + EscapeJsonString(property.name) + "\": \"" +
EscapeJsonString(texturePath) + "\"");
}
continue;
}
if (!property.serialized) {
continue;
}
const std::string propertyValueText = BuildMaterialPropertyValueText(property);
if (!propertyValueText.empty()) {
propertyEntries.push_back(
" \"" + EscapeJsonString(property.name) + "\": " + propertyValueText);
}
}
if (!propertyEntries.empty()) {
std::string propertiesObject = " \"properties\": {\n";
for (size_t propertyIndex = 0; propertyIndex < propertyEntries.size(); ++propertyIndex) {
propertiesObject += propertyEntries[propertyIndex];
if (propertyIndex + 1 < propertyEntries.size()) {
propertiesObject += ",";
}
propertiesObject += "\n";
}
propertiesObject += " }";
rootEntries.push_back(propertiesObject);
}
if (!textureEntries.empty()) {
std::string texturesObject = " \"textures\": {\n";
for (size_t textureIndex = 0; textureIndex < textureEntries.size(); ++textureIndex) {
texturesObject += textureEntries[textureIndex];
if (textureIndex + 1 < textureEntries.size()) {
texturesObject += ",";
}
texturesObject += "\n";
}
texturesObject += " }";
rootEntries.push_back(texturesObject);
}
if (state.hasRenderStateOverride) {
const ::XCEngine::Resources::MaterialRenderState& renderState = state.renderState;
std::vector<std::string> renderStateEntries;
renderStateEntries.push_back(
" \"cull\": \"" + std::string(SerializeCullMode(renderState.cullMode)) + "\"");
renderStateEntries.push_back(std::string(" \"depthTest\": ") + (renderState.depthTestEnable ? "true" : "false"));
renderStateEntries.push_back(std::string(" \"depthWrite\": ") + (renderState.depthWriteEnable ? "true" : "false"));
renderStateEntries.push_back(
" \"depthFunc\": \"" + std::string(SerializeComparisonFunc(renderState.depthFunc)) + "\"");
renderStateEntries.push_back(std::string(" \"blendEnable\": ") + (renderState.blendEnable ? "true" : "false"));
renderStateEntries.push_back(
" \"srcBlend\": \"" + std::string(SerializeBlendFactor(renderState.srcBlend)) + "\"");
renderStateEntries.push_back(
" \"dstBlend\": \"" + std::string(SerializeBlendFactor(renderState.dstBlend)) + "\"");
renderStateEntries.push_back(
" \"srcBlendAlpha\": \"" + std::string(SerializeBlendFactor(renderState.srcBlendAlpha)) + "\"");
renderStateEntries.push_back(
" \"dstBlendAlpha\": \"" + std::string(SerializeBlendFactor(renderState.dstBlendAlpha)) + "\"");
renderStateEntries.push_back(
" \"blendOp\": \"" + std::string(SerializeBlendOp(renderState.blendOp)) + "\"");
renderStateEntries.push_back(
" \"blendOpAlpha\": \"" + std::string(SerializeBlendOp(renderState.blendOpAlpha)) + "\"");
renderStateEntries.push_back(" \"colorWriteMask\": " + std::to_string(renderState.colorWriteMask));
if (renderState.depthBiasFactor != 0.0f || renderState.depthBiasUnits != 0) {
renderStateEntries.push_back(
" \"offset\": [" +
FormatJsonFloat(renderState.depthBiasFactor) + ", " +
std::to_string(renderState.depthBiasUnits) + "]");
}
if (renderState.stencil.enabled) {
std::vector<std::string> stencilEntries;
stencilEntries.push_back(" \"enabled\": true");
stencilEntries.push_back(" \"ref\": " + std::to_string(renderState.stencil.reference));
stencilEntries.push_back(" \"readMask\": " + std::to_string(renderState.stencil.readMask));
stencilEntries.push_back(" \"writeMask\": " + std::to_string(renderState.stencil.writeMask));
stencilEntries.push_back(
" \"comp\": \"" + std::string(SerializeComparisonFunc(renderState.stencil.front.func)) + "\"");
stencilEntries.push_back(
" \"pass\": \"" + std::string(SerializeStencilOp(renderState.stencil.front.passOp)) + "\"");
stencilEntries.push_back(
" \"fail\": \"" + std::string(SerializeStencilOp(renderState.stencil.front.failOp)) + "\"");
stencilEntries.push_back(
" \"zFail\": \"" + std::string(SerializeStencilOp(renderState.stencil.front.depthFailOp)) + "\"");
stencilEntries.push_back(
" \"compBack\": \"" + std::string(SerializeComparisonFunc(renderState.stencil.back.func)) + "\"");
stencilEntries.push_back(
" \"passBack\": \"" + std::string(SerializeStencilOp(renderState.stencil.back.passOp)) + "\"");
stencilEntries.push_back(
" \"failBack\": \"" + std::string(SerializeStencilOp(renderState.stencil.back.failOp)) + "\"");
stencilEntries.push_back(
" \"zFailBack\": \"" + std::string(SerializeStencilOp(renderState.stencil.back.depthFailOp)) + "\"");
std::string stencilObject = " \"stencil\": {\n";
for (size_t stencilIndex = 0; stencilIndex < stencilEntries.size(); ++stencilIndex) {
stencilObject += stencilEntries[stencilIndex];
if (stencilIndex + 1 < stencilEntries.size()) {
stencilObject += ",";
}
stencilObject += "\n";
}
stencilObject += " }";
renderStateEntries.push_back(stencilObject);
}
std::string renderStateObject = " \"renderState\": {\n";
for (size_t renderStateIndex = 0; renderStateIndex < renderStateEntries.size(); ++renderStateIndex) {
renderStateObject += renderStateEntries[renderStateIndex];
if (renderStateIndex + 1 < renderStateEntries.size()) {
renderStateObject += ",";
}
renderStateObject += "\n";
}
renderStateObject += " }";
rootEntries.push_back(renderStateObject);
}
std::string json = "{\n";
for (size_t entryIndex = 0; entryIndex < rootEntries.size(); ++entryIndex) {
json += rootEntries[entryIndex];
if (entryIndex + 1 < rootEntries.size()) {
json += ",";
}
json += "\n";
}
json += "}\n";
return json;
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,34 @@
#pragma once
#include "MaterialInspectorMaterialState.h"
#include <XCEngine/Core/Asset/ResourceHandle.h>
#include <string>
#include <vector>
namespace XCEngine {
namespace Resources {
class Material;
class Shader;
}
namespace Editor {
bool IsTextureMaterialPropertyType(::XCEngine::Resources::MaterialPropertyType type);
std::vector<MaterialPropertyState> CollectMaterialPropertyStates(
const ::XCEngine::Resources::Material& material);
void SyncMaterialAssetStateWithShader(
const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle,
MaterialAssetState& state);
void ApplyMaterialAuthoringPresenceToState(
const std::string& sourceText,
MaterialAssetState& state);
std::string BuildMaterialAssetFileText(const MaterialAssetState& state);
} // namespace Editor
} // namespace XCEngine