Refactor material inspector state IO
This commit is contained in:
@@ -522,3 +522,46 @@ Material 面板不应再次演化成随意堆字段的临时入口。所有 rend
|
||||
- 更完整的属性类型/显示策略覆盖
|
||||
|
||||
因此,Phase 4 的性质是“先把 authoring 语义做正确”,为最后的测试与收口创造条件。
|
||||
|
||||
## 15. Phase 5 执行结果
|
||||
|
||||
状态:已完成
|
||||
|
||||
本阶段重点不是继续扩材质面板功能,而是把已经形成的正式链路变成“可持续回归验证”的状态,并把测试面收口到 Inspector 实际依赖的核心逻辑上。
|
||||
|
||||
### 15.1 已完成内容
|
||||
|
||||
- 新增 `MaterialInspectorMaterialState` / `MaterialInspectorMaterialStateIO` 辅助模块,承载:
|
||||
- Material Inspector 状态结构
|
||||
- source authored presence 解析
|
||||
- Shader 切换时的属性/关键词重同步
|
||||
- 材质源文件序列化文本生成
|
||||
- 新增 `tests/editor/test_material_inspector_material_state_io.cpp`,覆盖:
|
||||
- authored 属性/纹理/关键词标记识别
|
||||
- Shader 切换时保留兼容 override、清理陈旧字段
|
||||
- 非 authored 默认值不写回源文件
|
||||
- 纹理 override 与 render state override 的正式序列化
|
||||
- `editor_tests` 构建链路已接入上述 helper 与新测试文件。
|
||||
|
||||
### 15.2 已执行验证
|
||||
|
||||
- `cmake --build build --config Debug --target XCEditor -j 8`
|
||||
- `cmake --build build --config Debug --target editor_tests -- /v:minimal`
|
||||
- `build/tests/Resources/Material/Debug/material_tests.exe`
|
||||
- `build/tests/Core/Asset/Debug/asset_tests.exe`
|
||||
- `build/tests/Editor/Debug/editor_tests.exe --gtest_filter=MaterialInspectorMaterialStateIOTest.*`
|
||||
|
||||
验证结果:
|
||||
|
||||
- `XCEditor` 编译通过
|
||||
- `material_tests`:61 / 61 通过
|
||||
- `asset_tests`:56 / 56 通过
|
||||
- `MaterialInspectorMaterialStateIOTest`:4 / 4 通过
|
||||
|
||||
### 15.3 当前剩余风险
|
||||
|
||||
- 全量 `editor_tests.exe` 在顺序执行 `EditorActionRoutingTest.*` 时仍存在既有的共享状态级别挂起现象。
|
||||
- 该挂起并非本次新增的材质面板测试本身触发:
|
||||
- 新增 `MaterialInspectorMaterialStateIOTest` 单独执行通过
|
||||
- `EditorActionRoutingTest.PlayModeAllowsRuntimeSceneUndoRedoButKeepsSceneDocumentCommandsBlocked` 单独执行通过
|
||||
- 因此,本阶段围绕 Material Inspector / Shader 属性面板的测试收口已经完成,但 Editor 其余历史测试链路仍需单独排查。
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
66
editor/src/panels/MaterialInspectorMaterialState.h
Normal file
66
editor/src/panels/MaterialInspectorMaterialState.h
Normal 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
|
||||
893
editor/src/panels/MaterialInspectorMaterialStateIO.cpp
Normal file
893
editor/src/panels/MaterialInspectorMaterialStateIO.cpp
Normal 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
|
||||
34
editor/src/panels/MaterialInspectorMaterialStateIO.h
Normal file
34
editor/src/panels/MaterialInspectorMaterialStateIO.h
Normal 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
|
||||
@@ -24,6 +24,7 @@ set(EDITOR_TEST_SOURCES
|
||||
test_scene_viewport_overlay_renderer.cpp
|
||||
test_scene_viewport_overlay_providers.cpp
|
||||
test_script_component_editor_utils.cpp
|
||||
test_viewport_panel_content.cpp
|
||||
test_viewport_host_surface_utils.cpp
|
||||
test_viewport_object_id_picker.cpp
|
||||
test_viewport_render_targets.cpp
|
||||
@@ -36,6 +37,7 @@ set(EDITOR_TEST_SOURCES
|
||||
test_editor_console_sink.cpp
|
||||
test_editor_script_assembly_builder.cpp
|
||||
test_editor_script_assembly_builder_utils.cpp
|
||||
test_material_inspector_material_state_io.cpp
|
||||
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui.cpp
|
||||
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_draw.cpp
|
||||
${CMAKE_BINARY_DIR}/_deps/imgui-src/imgui_tables.cpp
|
||||
@@ -46,6 +48,8 @@ set(EDITOR_TEST_SOURCES
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Core/PlaySessionController.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Managers/ProjectManager.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/panels/MaterialInspectorMaterialStateIO.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/UI/BuiltInIcons.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportPicker.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportMoveGizmo.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportRotateGizmo.cpp
|
||||
|
||||
308
tests/editor/test_material_inspector_material_state_io.cpp
Normal file
308
tests/editor/test_material_inspector_material_state_io.cpp
Normal file
@@ -0,0 +1,308 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "panels/MaterialInspectorMaterialState.h"
|
||||
#include "panels/MaterialInspectorMaterialStateIO.h"
|
||||
|
||||
#include <XCEngine/Core/Asset/IResource.h>
|
||||
#include <XCEngine/Core/Asset/ResourceHandle.h>
|
||||
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
||||
#include <XCEngine/Resources/Shader/Shader.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Editor::ApplyMaterialAuthoringPresenceToState;
|
||||
using XCEngine::Editor::BuildMaterialAssetFileText;
|
||||
using XCEngine::Editor::MaterialAssetState;
|
||||
using XCEngine::Editor::MaterialKeywordState;
|
||||
using XCEngine::Editor::MaterialPropertyState;
|
||||
using XCEngine::Editor::MaterialTagEditRow;
|
||||
using XCEngine::Editor::SyncMaterialAssetStateWithShader;
|
||||
using XCEngine::Resources::IResource;
|
||||
using XCEngine::Resources::MaterialBlendFactor;
|
||||
using XCEngine::Resources::MaterialBlendOp;
|
||||
using XCEngine::Resources::MaterialComparisonFunc;
|
||||
using XCEngine::Resources::MaterialCullMode;
|
||||
using XCEngine::Resources::MaterialPropertyType;
|
||||
using XCEngine::Resources::MaterialRenderQueue;
|
||||
using XCEngine::Resources::ResourceGUID;
|
||||
using XCEngine::Resources::ResourceHandle;
|
||||
using XCEngine::Resources::Shader;
|
||||
using XCEngine::Resources::ShaderKeywordDeclaration;
|
||||
using XCEngine::Resources::ShaderKeywordDeclarationType;
|
||||
using XCEngine::Resources::ShaderPass;
|
||||
using XCEngine::Resources::ShaderPropertyDesc;
|
||||
using XCEngine::Resources::ShaderPropertyType;
|
||||
|
||||
template <size_t N>
|
||||
void CopyText(const std::string& value, std::array<char, N>& buffer) {
|
||||
buffer.fill('\0');
|
||||
const size_t copyLength = (std::min)(value.size(), N - 1);
|
||||
if (copyLength > 0) {
|
||||
std::memcpy(buffer.data(), value.data(), copyLength);
|
||||
}
|
||||
buffer[copyLength] = '\0';
|
||||
}
|
||||
|
||||
MaterialPropertyState* FindProperty(MaterialAssetState& state, const char* name) {
|
||||
for (MaterialPropertyState& property : state.properties) {
|
||||
if (property.name == name) {
|
||||
return &property;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<Shader> CreateSchemaShader(
|
||||
const char* path,
|
||||
std::initializer_list<const char*> declaredKeywords = {}) {
|
||||
auto shader = std::make_unique<Shader>();
|
||||
|
||||
IResource::ConstructParams params = {};
|
||||
params.name = "InspectorTestShader";
|
||||
params.path = path;
|
||||
params.guid = ResourceGUID::Generate(path);
|
||||
shader->Initialize(params);
|
||||
|
||||
ShaderPropertyDesc baseColor = {};
|
||||
baseColor.name = "_BaseColor";
|
||||
baseColor.displayName = "Base Color";
|
||||
baseColor.type = ShaderPropertyType::Color;
|
||||
baseColor.defaultValue = "(1,1,1,1)";
|
||||
shader->AddProperty(baseColor);
|
||||
|
||||
ShaderPropertyDesc metallic = {};
|
||||
metallic.name = "_Metallic";
|
||||
metallic.displayName = "Metallic";
|
||||
metallic.type = ShaderPropertyType::Float;
|
||||
metallic.defaultValue = "0.7";
|
||||
shader->AddProperty(metallic);
|
||||
|
||||
ShaderPropertyDesc mode = {};
|
||||
mode.name = "_Mode";
|
||||
mode.displayName = "Mode";
|
||||
mode.type = ShaderPropertyType::Int;
|
||||
mode.defaultValue = "2";
|
||||
shader->AddProperty(mode);
|
||||
|
||||
ShaderPropertyDesc mainTex = {};
|
||||
mainTex.name = "_MainTex";
|
||||
mainTex.displayName = "Main Tex";
|
||||
mainTex.type = ShaderPropertyType::Texture2D;
|
||||
mainTex.defaultValue = "white";
|
||||
shader->AddProperty(mainTex);
|
||||
|
||||
ShaderPass pass = {};
|
||||
pass.name = "ForwardLit";
|
||||
shader->AddPass(pass);
|
||||
|
||||
if (declaredKeywords.size() > 0) {
|
||||
ShaderKeywordDeclaration declaration = {};
|
||||
declaration.type = ShaderKeywordDeclarationType::ShaderFeatureLocal;
|
||||
declaration.options.PushBack("_");
|
||||
for (const char* keyword : declaredKeywords) {
|
||||
declaration.options.PushBack(keyword);
|
||||
}
|
||||
shader->AddPassKeywordDeclaration("ForwardLit", declaration);
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
TEST(MaterialInspectorMaterialStateIOTest, ApplyMaterialAuthoringPresenceMarksOnlySourceAuthoredEntries) {
|
||||
MaterialAssetState state;
|
||||
state.keywords = {
|
||||
MaterialKeywordState{ "XC_ALPHA_TEST", false },
|
||||
MaterialKeywordState{ "XC_UNUSED", false }
|
||||
};
|
||||
|
||||
MaterialPropertyState metallic = {};
|
||||
metallic.name = "_Metallic";
|
||||
metallic.type = MaterialPropertyType::Float;
|
||||
state.properties.push_back(metallic);
|
||||
|
||||
MaterialPropertyState baseColor = {};
|
||||
baseColor.name = "_BaseColor";
|
||||
baseColor.type = MaterialPropertyType::Float4;
|
||||
state.properties.push_back(baseColor);
|
||||
|
||||
MaterialPropertyState mainTex = {};
|
||||
mainTex.name = "_MainTex";
|
||||
mainTex.type = MaterialPropertyType::Texture;
|
||||
state.properties.push_back(mainTex);
|
||||
|
||||
ApplyMaterialAuthoringPresenceToState(
|
||||
"{\n"
|
||||
" \"keywords\": [\"XC_ALPHA_TEST\"],\n"
|
||||
" \"properties\": { \"_Metallic\": 0.35 },\n"
|
||||
" \"textures\": { \"_MainTex\": \"Assets/Textures/albedo.png\" }\n"
|
||||
"}\n",
|
||||
state);
|
||||
|
||||
EXPECT_FALSE(state.hasRenderStateOverride);
|
||||
ASSERT_EQ(state.keywords.size(), 2u);
|
||||
EXPECT_TRUE(state.keywords[0].serialized);
|
||||
EXPECT_FALSE(state.keywords[1].serialized);
|
||||
|
||||
ASSERT_NE(FindProperty(state, "_Metallic"), nullptr);
|
||||
ASSERT_NE(FindProperty(state, "_BaseColor"), nullptr);
|
||||
ASSERT_NE(FindProperty(state, "_MainTex"), nullptr);
|
||||
EXPECT_TRUE(FindProperty(state, "_Metallic")->serialized);
|
||||
EXPECT_FALSE(FindProperty(state, "_BaseColor")->serialized);
|
||||
EXPECT_TRUE(FindProperty(state, "_MainTex")->serialized);
|
||||
}
|
||||
|
||||
TEST(MaterialInspectorMaterialStateIOTest, SyncMaterialAssetStateWithShaderPreservesCompatibleOverridesAndDropsStaleData) {
|
||||
auto shader = CreateSchemaShader(
|
||||
"memory://material_inspector_sync_shader",
|
||||
{ "XC_ALPHA_TEST" });
|
||||
ResourceHandle<Shader> shaderHandle(shader.get());
|
||||
|
||||
MaterialAssetState state;
|
||||
|
||||
MaterialPropertyState legacy = {};
|
||||
legacy.name = "_Legacy";
|
||||
legacy.type = MaterialPropertyType::Float;
|
||||
legacy.floatValue[0] = 9.0f;
|
||||
legacy.serialized = true;
|
||||
state.properties.push_back(legacy);
|
||||
|
||||
MaterialPropertyState metallic = {};
|
||||
metallic.name = "_Metallic";
|
||||
metallic.type = MaterialPropertyType::Float;
|
||||
metallic.floatValue[0] = 0.33f;
|
||||
metallic.serialized = true;
|
||||
state.properties.push_back(metallic);
|
||||
|
||||
MaterialPropertyState mainTex = {};
|
||||
mainTex.name = "_MainTex";
|
||||
mainTex.type = MaterialPropertyType::Texture;
|
||||
mainTex.texturePath = "Assets/Textures/albedo.png";
|
||||
mainTex.serialized = true;
|
||||
state.properties.push_back(mainTex);
|
||||
|
||||
state.keywords.push_back(MaterialKeywordState{ "XC_ALPHA_TEST", true });
|
||||
state.keywords.push_back(MaterialKeywordState{ "XC_OLD", true });
|
||||
|
||||
SyncMaterialAssetStateWithShader(shaderHandle, state);
|
||||
|
||||
std::vector<std::string> propertyNames;
|
||||
propertyNames.reserve(state.properties.size());
|
||||
for (const MaterialPropertyState& property : state.properties) {
|
||||
propertyNames.push_back(property.name);
|
||||
}
|
||||
|
||||
const std::vector<std::string> expectedNames = {
|
||||
"_BaseColor",
|
||||
"_Metallic",
|
||||
"_Mode",
|
||||
"_MainTex"
|
||||
};
|
||||
EXPECT_EQ(propertyNames, expectedNames);
|
||||
|
||||
ASSERT_NE(FindProperty(state, "_Metallic"), nullptr);
|
||||
ASSERT_NE(FindProperty(state, "_MainTex"), nullptr);
|
||||
EXPECT_FLOAT_EQ(FindProperty(state, "_Metallic")->floatValue[0], 0.33f);
|
||||
EXPECT_TRUE(FindProperty(state, "_Metallic")->serialized);
|
||||
EXPECT_EQ(FindProperty(state, "_MainTex")->texturePath, "Assets/Textures/albedo.png");
|
||||
EXPECT_TRUE(FindProperty(state, "_MainTex")->serialized);
|
||||
EXPECT_EQ(FindProperty(state, "_Mode")->intValue[0], 2);
|
||||
EXPECT_EQ(FindProperty(state, "_Legacy"), nullptr);
|
||||
|
||||
ASSERT_EQ(state.keywords.size(), 1u);
|
||||
EXPECT_EQ(state.keywords[0].value, "XC_ALPHA_TEST");
|
||||
EXPECT_TRUE(state.keywords[0].serialized);
|
||||
}
|
||||
|
||||
TEST(MaterialInspectorMaterialStateIOTest, BuildMaterialAssetFileTextOmitsNonSerializedDefaultsAndDisabledRenderState) {
|
||||
MaterialAssetState state;
|
||||
CopyText("Assets/Shaders/test.shader", state.shaderPath);
|
||||
state.renderQueue = static_cast<int>(MaterialRenderQueue::Geometry);
|
||||
|
||||
MaterialTagEditRow tag = {};
|
||||
CopyText("RenderType", tag.name);
|
||||
CopyText("Opaque", tag.value);
|
||||
state.tags.push_back(tag);
|
||||
|
||||
state.keywords.push_back(MaterialKeywordState{ "XC_ALPHA_TEST", true });
|
||||
state.keywords.push_back(MaterialKeywordState{ "XC_UNUSED", false });
|
||||
|
||||
MaterialPropertyState metallic = {};
|
||||
metallic.name = "_Metallic";
|
||||
metallic.type = MaterialPropertyType::Float;
|
||||
metallic.floatValue[0] = 0.25f;
|
||||
metallic.serialized = true;
|
||||
state.properties.push_back(metallic);
|
||||
|
||||
MaterialPropertyState baseColor = {};
|
||||
baseColor.name = "_BaseColor";
|
||||
baseColor.type = MaterialPropertyType::Float4;
|
||||
baseColor.floatValue = { 1.0f, 0.2f, 0.1f, 1.0f };
|
||||
baseColor.serialized = false;
|
||||
state.properties.push_back(baseColor);
|
||||
|
||||
MaterialPropertyState mainTex = {};
|
||||
mainTex.name = "_MainTex";
|
||||
mainTex.type = MaterialPropertyType::Texture;
|
||||
mainTex.texturePath = "Assets/Textures/albedo.png";
|
||||
mainTex.serialized = false;
|
||||
state.properties.push_back(mainTex);
|
||||
|
||||
const std::string text = BuildMaterialAssetFileText(state);
|
||||
|
||||
EXPECT_NE(text.find("\"shader\": \"Assets/Shaders/test.shader\""), std::string::npos);
|
||||
EXPECT_NE(text.find("\"renderQueue\": \"geometry\""), std::string::npos);
|
||||
EXPECT_NE(text.find("\"RenderType\": \"Opaque\""), std::string::npos);
|
||||
EXPECT_NE(text.find("\"keywords\": [\"XC_ALPHA_TEST\"]"), std::string::npos);
|
||||
EXPECT_NE(text.find("\"_Metallic\": 0.25"), std::string::npos);
|
||||
EXPECT_EQ(text.find("\"_BaseColor\""), std::string::npos);
|
||||
EXPECT_EQ(text.find("\"textures\""), std::string::npos);
|
||||
EXPECT_EQ(text.find("\"renderState\""), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(MaterialInspectorMaterialStateIOTest, BuildMaterialAssetFileTextWritesSerializedTextureAndRenderStateOverrides) {
|
||||
MaterialAssetState state;
|
||||
CopyText("Assets/Shaders/test.shader", state.shaderPath);
|
||||
state.renderQueue = 2051;
|
||||
state.hasRenderStateOverride = true;
|
||||
state.renderState.cullMode = MaterialCullMode::Back;
|
||||
state.renderState.depthTestEnable = true;
|
||||
state.renderState.depthWriteEnable = false;
|
||||
state.renderState.depthFunc = MaterialComparisonFunc::LessEqual;
|
||||
state.renderState.blendEnable = true;
|
||||
state.renderState.srcBlend = MaterialBlendFactor::SrcAlpha;
|
||||
state.renderState.dstBlend = MaterialBlendFactor::InvSrcAlpha;
|
||||
state.renderState.srcBlendAlpha = MaterialBlendFactor::One;
|
||||
state.renderState.dstBlendAlpha = MaterialBlendFactor::InvSrcAlpha;
|
||||
state.renderState.blendOp = MaterialBlendOp::Add;
|
||||
state.renderState.blendOpAlpha = MaterialBlendOp::Add;
|
||||
state.renderState.colorWriteMask = 7;
|
||||
|
||||
state.keywords.push_back(MaterialKeywordState{ "XC_ALPHA_TEST", true });
|
||||
|
||||
MaterialPropertyState mainTex = {};
|
||||
mainTex.name = "_MainTex";
|
||||
mainTex.type = MaterialPropertyType::Texture;
|
||||
mainTex.texturePath = "Assets/Textures/albedo.png";
|
||||
mainTex.serialized = true;
|
||||
state.properties.push_back(mainTex);
|
||||
|
||||
const std::string text = BuildMaterialAssetFileText(state);
|
||||
|
||||
EXPECT_NE(text.find("\"renderQueue\": 2051"), std::string::npos);
|
||||
EXPECT_NE(text.find("\"keywords\": [\"XC_ALPHA_TEST\"]"), std::string::npos);
|
||||
EXPECT_NE(text.find("\"textures\": {"), std::string::npos);
|
||||
EXPECT_NE(text.find("\"_MainTex\": \"Assets/Textures/albedo.png\""), std::string::npos);
|
||||
EXPECT_NE(text.find("\"renderState\": {"), std::string::npos);
|
||||
EXPECT_NE(text.find("\"cull\": \"back\""), std::string::npos);
|
||||
EXPECT_NE(text.find("\"blendEnable\": true"), std::string::npos);
|
||||
EXPECT_NE(text.find("\"srcBlend\": \"src_alpha\""), std::string::npos);
|
||||
EXPECT_NE(text.find("\"colorWriteMask\": 7"), std::string::npos);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
103
tests/editor/test_viewport_panel_content.cpp
Normal file
103
tests/editor/test_viewport_panel_content.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "panels/ViewportPanelContent.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace {
|
||||
|
||||
class ImGuiContextScope {
|
||||
public:
|
||||
ImGuiContextScope() {
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
}
|
||||
|
||||
~ImGuiContextScope() {
|
||||
ImGui::DestroyContext();
|
||||
}
|
||||
};
|
||||
|
||||
void BeginTestFrame() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.DisplaySize = ImVec2(640.0f, 480.0f);
|
||||
io.DeltaTime = 1.0f / 60.0f;
|
||||
unsigned char* fontPixels = nullptr;
|
||||
int fontWidth = 0;
|
||||
int fontHeight = 0;
|
||||
io.Fonts->GetTexDataAsRGBA32(&fontPixels, &fontWidth, &fontHeight);
|
||||
io.Fonts->SetTexID(static_cast<ImTextureID>(1));
|
||||
ImGui::GetStyle().WindowPadding = ImVec2(0.0f, 0.0f);
|
||||
|
||||
ImGui::NewFrame();
|
||||
ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f));
|
||||
ImGui::SetNextWindowSize(ImVec2(320.0f, 240.0f));
|
||||
ASSERT_TRUE(ImGui::Begin(
|
||||
"ViewportPanelContentTestWindow",
|
||||
nullptr,
|
||||
ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoSavedSettings));
|
||||
}
|
||||
|
||||
void EndTestFrame() {
|
||||
ImGui::End();
|
||||
ImGui::EndFrame();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ViewportPanelContentTest, AllowOverlapDefaultItemClickMissesMiddleClickOnFirstFrame) {
|
||||
ImGuiContextScope contextScope;
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
BeginTestFrame();
|
||||
EndTestFrame();
|
||||
|
||||
io.MousePos = ImVec2(32.0f, 32.0f);
|
||||
io.MouseDown[ImGuiMouseButton_Middle] = true;
|
||||
BeginTestFrame();
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(16.0f, 16.0f));
|
||||
|
||||
ImGui::SetNextItemAllowOverlap();
|
||||
ImGui::InvisibleButton(
|
||||
"##ViewportOverlapProbe",
|
||||
ImVec2(120.0f, 90.0f),
|
||||
ImGuiButtonFlags_MouseButtonMiddle);
|
||||
|
||||
EXPECT_FALSE(ImGui::IsItemHovered());
|
||||
EXPECT_FALSE(ImGui::IsItemClicked(ImGuiMouseButton_Middle));
|
||||
EXPECT_TRUE(ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByItem));
|
||||
|
||||
EndTestFrame();
|
||||
}
|
||||
|
||||
TEST(ViewportPanelContentTest, InteractionSurfaceCapturesMiddleClickOnFirstHoveredFrame) {
|
||||
ImGuiContextScope contextScope;
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
BeginTestFrame();
|
||||
EndTestFrame();
|
||||
|
||||
io.MousePos = ImVec2(32.0f, 32.0f);
|
||||
io.MouseDown[ImGuiMouseButton_Middle] = true;
|
||||
BeginTestFrame();
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(16.0f, 16.0f));
|
||||
|
||||
XCEngine::Editor::ViewportPanelContentResult result = {};
|
||||
XCEngine::Editor::RenderViewportInteractionSurface(
|
||||
result,
|
||||
XCEngine::Editor::EditorViewportKind::Scene,
|
||||
ImVec2(120.0f, 90.0f));
|
||||
|
||||
EXPECT_TRUE(result.hovered);
|
||||
EXPECT_TRUE(result.clickedMiddle);
|
||||
EXPECT_FALSE(result.clickedLeft);
|
||||
EXPECT_FALSE(result.clickedRight);
|
||||
|
||||
EndTestFrame();
|
||||
}
|
||||
Reference in New Issue
Block a user