diff --git a/editor/src/panels/InspectorPanel.cpp b/editor/src/panels/InspectorPanel.cpp index f88f426a..27ab1f00 100644 --- a/editor/src/panels/InspectorPanel.cpp +++ b/editor/src/panels/InspectorPanel.cpp @@ -448,7 +448,7 @@ void ApplyMaterialKeywordsToResource( material.ClearKeywords(); for (const InspectorPanel::MaterialKeywordState& keyword : state.keywords) { const std::string keywordValue = TrimCopy(keyword.value); - if (!keywordValue.empty()) { + if (!keywordValue.empty() && keyword.serialized) { material.EnableKeyword(keywordValue.c_str()); } } @@ -595,6 +595,7 @@ bool DrawFloat4Property(const char* label, float values[4]) { bool DrawShaderDrivenMaterialProperty( const ::XCEngine::Resources::ShaderPropertyDesc& shaderProperty, + const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle, InspectorPanel::MaterialAssetState& state) { InspectorPanel::MaterialPropertyState* propertyState = FindMaterialPropertyState(state, shaderProperty.name); if (propertyState == nullptr) { @@ -605,62 +606,63 @@ bool DrawShaderDrivenMaterialProperty( ? shaderProperty.name.CStr() : shaderProperty.displayName.CStr(); + bool changed = false; switch (shaderProperty.type) { case ::XCEngine::Resources::ShaderPropertyType::Float: case ::XCEngine::Resources::ShaderPropertyType::Range: - if (!UI::DrawPropertyFloat(label, propertyState->floatValue[0], 0.01f)) { - return false; + if (UI::DrawPropertyFloat(label, propertyState->floatValue[0], 0.01f)) { + propertyState->serialized = true; + changed = true; } - propertyState->serialized = true; - return true; + break; case ::XCEngine::Resources::ShaderPropertyType::Int: { int value = propertyState->intValue[0]; - if (!UI::DrawPropertyInt(label, value, 1)) { - return false; + if (UI::DrawPropertyInt(label, value, 1)) { + propertyState->intValue[0] = value; + propertyState->serialized = true; + changed = true; } - propertyState->intValue[0] = value; - propertyState->serialized = true; - return true; + break; } case ::XCEngine::Resources::ShaderPropertyType::Color: - if (!UI::DrawPropertyColor4(label, propertyState->floatValue.data())) { - return false; + if (UI::DrawPropertyColor4(label, propertyState->floatValue.data())) { + propertyState->serialized = true; + changed = true; } - propertyState->serialized = true; - return true; + break; case ::XCEngine::Resources::ShaderPropertyType::Vector: { if (propertyState->type == ::XCEngine::Resources::MaterialPropertyType::Float2) { ::XCEngine::Math::Vector2 value(propertyState->floatValue[0], propertyState->floatValue[1]); - if (!UI::DrawPropertyVec2(label, value)) { - return false; + if (UI::DrawPropertyVec2(label, value)) { + propertyState->floatValue[0] = value.x; + propertyState->floatValue[1] = value.y; + propertyState->serialized = true; + changed = true; } - propertyState->floatValue[0] = value.x; - propertyState->floatValue[1] = value.y; - propertyState->serialized = true; - return true; + break; } if (propertyState->type == ::XCEngine::Resources::MaterialPropertyType::Float3) { ::XCEngine::Math::Vector3 value( propertyState->floatValue[0], propertyState->floatValue[1], propertyState->floatValue[2]); - if (!UI::DrawPropertyVec3(label, value)) { - return false; + if (UI::DrawPropertyVec3(label, value)) { + propertyState->floatValue[0] = value.x; + propertyState->floatValue[1] = value.y; + propertyState->floatValue[2] = value.z; + propertyState->serialized = true; + changed = true; } - propertyState->floatValue[0] = value.x; - propertyState->floatValue[1] = value.y; - propertyState->floatValue[2] = value.z; + break; + } + if (DrawFloat4Property(label, propertyState->floatValue.data())) { propertyState->serialized = true; - return true; + changed = true; } - if (!DrawFloat4Property(label, propertyState->floatValue.data())) { - return false; - } - propertyState->serialized = true; - return true; + break; } case ::XCEngine::Resources::ShaderPropertyType::Texture2D: @@ -672,25 +674,39 @@ bool DrawShaderDrivenMaterialProperty( "Select Texture Asset", { ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds", ".hdr", ".ppm" }); if (textureInteraction.clearRequested) { - if (propertyState->texturePath.empty()) { - return false; + if (!propertyState->texturePath.empty()) { + propertyState->texturePath.clear(); + propertyState->serialized = false; + changed = true; } - propertyState->texturePath.clear(); - propertyState->serialized = false; - return true; - } - if (!textureInteraction.assignedPath.empty() && - textureInteraction.assignedPath != propertyState->texturePath) { + } else if (!textureInteraction.assignedPath.empty() && + textureInteraction.assignedPath != propertyState->texturePath) { propertyState->texturePath = textureInteraction.assignedPath; propertyState->serialized = true; - return true; + changed = true; } - return false; + break; } default: return false; } + + ImGui::PushID(label); + if (propertyState->serialized) { + if (ImGui::SmallButton("Reset to Default")) { + if (ResetMaterialPropertyStateToShaderDefault(shaderHandle, propertyState->name, state)) { + changed = true; + } + } + ImGui::SameLine(); + UI::DrawHintText("Material Override"); + } else { + UI::DrawHintText("Inherited from Shader"); + } + ImGui::PopID(); + + return changed; } std::string BuildMaterialLoadFailureMessage(const ::XCEngine::Resources::LoadResult& result) { @@ -805,10 +821,16 @@ void InspectorPanel::PopulateMaterialAssetStateFromResource(::XCEngine::Resource for (Core::uint32 keywordIndex = 0; keywordIndex < material.GetKeywordCount(); ++keywordIndex) { MaterialKeywordState keywordState; keywordState.value = std::string(material.GetKeyword(keywordIndex).CStr()); + keywordState.serialized = true; m_materialAssetState.keywords.push_back(std::move(keywordState)); } m_materialAssetState.properties = CollectMaterialPropertyStates(material); + if (::XCEngine::Resources::Shader* shader = material.GetShader()) { + SyncMaterialAssetStateWithShader( + ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>(shader), + m_materialAssetState); + } const std::string sourceText = ReadTextFileOrEmpty(m_materialAssetState.assetFullPath); if (!sourceText.empty()) { @@ -1234,7 +1256,9 @@ void InspectorPanel::RenderMaterialAsset() { } else { bool changed = false; for (const ::XCEngine::Resources::ShaderPropertyDesc& shaderProperty : materialShaderHandle->GetProperties()) { - changed = DrawShaderDrivenMaterialProperty(shaderProperty, m_materialAssetState) || changed; + changed = + DrawShaderDrivenMaterialProperty(shaderProperty, materialShaderHandle, m_materialAssetState) || + changed; if (!shaderProperty.defaultValue.Empty()) { const std::string defaultText = std::string("Default: ") + shaderProperty.defaultValue.CStr(); @@ -1251,6 +1275,35 @@ void InspectorPanel::RenderMaterialAsset() { UI::EndComponentSection(propertiesSection); } + const UI::ComponentSectionResult keywordsSection = UI::BeginComponentSection( + "MaterialAssetKeywords", + "Keywords"); + if (keywordsSection.open) { + ImGui::Indent(keywordsSection.contentIndent); + + if (!hasMaterialShader) { + UI::DrawHintText("Select a shader to expose shader keywords."); + } else if (m_materialAssetState.keywords.empty()) { + UI::DrawHintText("Selected shader does not declare editable keywords."); + } else { + bool changed = false; + for (MaterialKeywordState& keyword : m_materialAssetState.keywords) { + bool enabled = keyword.serialized; + if (UI::DrawPropertyBool(keyword.value.c_str(), enabled)) { + keyword.serialized = enabled; + changed = true; + } + } + + if (changed) { + m_materialAssetState.dirty = true; + SaveMaterialAsset(); + } + } + + UI::EndComponentSection(keywordsSection); + } + const UI::ComponentSectionResult renderStateSection = UI::BeginComponentSection( "MaterialAssetRenderState", "Render State"); diff --git a/editor/src/panels/MaterialInspectorMaterialStateIO.cpp b/editor/src/panels/MaterialInspectorMaterialStateIO.cpp index 39be9d81..4b04832a 100644 --- a/editor/src/panels/MaterialInspectorMaterialStateIO.cpp +++ b/editor/src/panels/MaterialInspectorMaterialStateIO.cpp @@ -423,6 +423,52 @@ std::vector BuildShaderDefaultPropertyStates( return CollectMaterialPropertyStates(scratchMaterial); } +std::vector BuildShaderKeywordStates( + const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle, + const std::vector& previousKeywords) { + if (!shaderHandle.IsValid() || shaderHandle.Get() == nullptr) { + return {}; + } + + std::unordered_map previousKeywordStates; + previousKeywordStates.reserve(previousKeywords.size()); + for (const MaterialKeywordState& keyword : previousKeywords) { + const std::string keywordValue = TrimCopy(keyword.value); + if (!keywordValue.empty()) { + previousKeywordStates.emplace(keywordValue, keyword); + } + } + + std::vector keywordStates; + std::unordered_set seenKeywords; + for (const ::XCEngine::Resources::ShaderPass& pass : shaderHandle->GetPasses()) { + for (const ::XCEngine::Resources::ShaderKeywordDeclaration& declaration : pass.keywordDeclarations) { + for (const Containers::String& option : declaration.options) { + const Containers::String normalizedKeyword = + ::XCEngine::Resources::NormalizeShaderKeywordToken(option); + if (normalizedKeyword.Empty()) { + continue; + } + + const std::string keywordValue(normalizedKeyword.CStr()); + if (!seenKeywords.insert(keywordValue).second) { + continue; + } + + MaterialKeywordState state; + state.value = keywordValue; + const auto previousKeywordIt = previousKeywordStates.find(keywordValue); + if (previousKeywordIt != previousKeywordStates.end()) { + state.serialized = previousKeywordIt->second.serialized; + } + keywordStates.push_back(std::move(state)); + } + } + } + + return keywordStates; +} + constexpr int kCustomRenderQueuePresetIndex = 5; int ResolveRenderQueuePresetIndex(int renderQueue) { @@ -652,16 +698,40 @@ void SyncMaterialAssetStateWithShader( CopyMaterialPropertyValue(previousPropertyIt->second, property); } state.properties = std::move(nextProperties); + state.keywords = BuildShaderKeywordStates(shaderHandle, state.keywords); +} - std::vector 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); +bool ResetMaterialPropertyStateToShaderDefault( + const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle, + const std::string& propertyName, + MaterialAssetState& state) { + if (!shaderHandle.IsValid() || shaderHandle.Get() == nullptr || propertyName.empty()) { + return false; + } + + MaterialPropertyState* stateProperty = nullptr; + for (MaterialPropertyState& property : state.properties) { + if (property.name == propertyName) { + stateProperty = &property; + break; } } - state.keywords = std::move(nextKeywords); + if (stateProperty == nullptr) { + return false; + } + + const std::vector defaultProperties = BuildShaderDefaultPropertyStates(shaderHandle); + for (const MaterialPropertyState& defaultProperty : defaultProperties) { + if (defaultProperty.name != propertyName || defaultProperty.type != stateProperty->type) { + continue; + } + + CopyMaterialPropertyValue(defaultProperty, *stateProperty); + stateProperty->serialized = false; + return true; + } + + return false; } void ApplyMaterialAuthoringPresenceToState( diff --git a/editor/src/panels/MaterialInspectorMaterialStateIO.h b/editor/src/panels/MaterialInspectorMaterialStateIO.h index 1c78d6e1..a29b6839 100644 --- a/editor/src/panels/MaterialInspectorMaterialStateIO.h +++ b/editor/src/panels/MaterialInspectorMaterialStateIO.h @@ -24,6 +24,11 @@ void SyncMaterialAssetStateWithShader( const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle, MaterialAssetState& state); +bool ResetMaterialPropertyStateToShaderDefault( + const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle, + const std::string& propertyName, + MaterialAssetState& state); + void ApplyMaterialAuthoringPresenceToState( const std::string& sourceText, MaterialAssetState& state); diff --git a/tests/editor/test_material_inspector_material_state_io.cpp b/tests/editor/test_material_inspector_material_state_io.cpp index 89ae422f..69ac04c9 100644 --- a/tests/editor/test_material_inspector_material_state_io.cpp +++ b/tests/editor/test_material_inspector_material_state_io.cpp @@ -22,6 +22,7 @@ using XCEngine::Editor::MaterialAssetState; using XCEngine::Editor::MaterialKeywordState; using XCEngine::Editor::MaterialPropertyState; using XCEngine::Editor::MaterialTagEditRow; +using XCEngine::Editor::ResetMaterialPropertyStateToShaderDefault; using XCEngine::Editor::SyncMaterialAssetStateWithShader; using XCEngine::Resources::IResource; using XCEngine::Resources::MaterialBlendFactor; @@ -219,6 +220,41 @@ TEST(MaterialInspectorMaterialStateIOTest, SyncMaterialAssetStateWithShaderPrese EXPECT_TRUE(state.keywords[0].serialized); } +TEST(MaterialInspectorMaterialStateIOTest, SyncMaterialAssetStateWithShaderAddsDeclaredKeywordsAsDisabledEntries) { + auto shader = CreateSchemaShader( + "memory://material_inspector_keywords_shader", + { "XC_ALPHA_TEST", "XC_DETAIL" }); + ResourceHandle shaderHandle(shader.get()); + + MaterialAssetState state; + state.keywords.push_back(MaterialKeywordState{ "XC_ALPHA_TEST", true }); + + SyncMaterialAssetStateWithShader(shaderHandle, state); + + ASSERT_EQ(state.keywords.size(), 2u); + EXPECT_EQ(state.keywords[0].value, "XC_ALPHA_TEST"); + EXPECT_TRUE(state.keywords[0].serialized); + EXPECT_EQ(state.keywords[1].value, "XC_DETAIL"); + EXPECT_FALSE(state.keywords[1].serialized); +} + +TEST(MaterialInspectorMaterialStateIOTest, ResetMaterialPropertyStateToShaderDefaultRestoresDefaultAndClearsOverride) { + auto shader = CreateSchemaShader("memory://material_inspector_reset_shader"); + ResourceHandle shaderHandle(shader.get()); + + MaterialAssetState state; + SyncMaterialAssetStateWithShader(shaderHandle, state); + + MaterialPropertyState* metallic = FindProperty(state, "_Metallic"); + ASSERT_NE(metallic, nullptr); + metallic->floatValue[0] = 0.15f; + metallic->serialized = true; + + ASSERT_TRUE(ResetMaterialPropertyStateToShaderDefault(shaderHandle, "_Metallic", state)); + EXPECT_FLOAT_EQ(metallic->floatValue[0], 0.7f); + EXPECT_FALSE(metallic->serialized); +} + TEST(MaterialInspectorMaterialStateIOTest, BuildMaterialAssetFileTextOmitsNonSerializedDefaultsAndDisabledRenderState) { MaterialAssetState state; CopyText("Assets/Shaders/test.shader", state.shaderPath);