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

@@ -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

View 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

View 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();
}