Complete material inspector keyword and reset flow

This commit is contained in:
2026-04-08 01:01:28 +08:00
parent b5bc9d41cb
commit 077a6b0a51
4 changed files with 213 additions and 49 deletions

View File

@@ -448,7 +448,7 @@ void ApplyMaterialKeywordsToResource(
material.ClearKeywords(); material.ClearKeywords();
for (const InspectorPanel::MaterialKeywordState& keyword : state.keywords) { for (const InspectorPanel::MaterialKeywordState& keyword : state.keywords) {
const std::string keywordValue = TrimCopy(keyword.value); const std::string keywordValue = TrimCopy(keyword.value);
if (!keywordValue.empty()) { if (!keywordValue.empty() && keyword.serialized) {
material.EnableKeyword(keywordValue.c_str()); material.EnableKeyword(keywordValue.c_str());
} }
} }
@@ -595,6 +595,7 @@ bool DrawFloat4Property(const char* label, float values[4]) {
bool DrawShaderDrivenMaterialProperty( bool DrawShaderDrivenMaterialProperty(
const ::XCEngine::Resources::ShaderPropertyDesc& shaderProperty, const ::XCEngine::Resources::ShaderPropertyDesc& shaderProperty,
const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle,
InspectorPanel::MaterialAssetState& state) { InspectorPanel::MaterialAssetState& state) {
InspectorPanel::MaterialPropertyState* propertyState = FindMaterialPropertyState(state, shaderProperty.name); InspectorPanel::MaterialPropertyState* propertyState = FindMaterialPropertyState(state, shaderProperty.name);
if (propertyState == nullptr) { if (propertyState == nullptr) {
@@ -605,62 +606,63 @@ bool DrawShaderDrivenMaterialProperty(
? shaderProperty.name.CStr() ? shaderProperty.name.CStr()
: shaderProperty.displayName.CStr(); : shaderProperty.displayName.CStr();
bool changed = false;
switch (shaderProperty.type) { switch (shaderProperty.type) {
case ::XCEngine::Resources::ShaderPropertyType::Float: case ::XCEngine::Resources::ShaderPropertyType::Float:
case ::XCEngine::Resources::ShaderPropertyType::Range: case ::XCEngine::Resources::ShaderPropertyType::Range:
if (!UI::DrawPropertyFloat(label, propertyState->floatValue[0], 0.01f)) { if (UI::DrawPropertyFloat(label, propertyState->floatValue[0], 0.01f)) {
return false;
}
propertyState->serialized = true; propertyState->serialized = true;
return true; changed = true;
}
break;
case ::XCEngine::Resources::ShaderPropertyType::Int: { case ::XCEngine::Resources::ShaderPropertyType::Int: {
int value = propertyState->intValue[0]; int value = propertyState->intValue[0];
if (!UI::DrawPropertyInt(label, value, 1)) { if (UI::DrawPropertyInt(label, value, 1)) {
return false;
}
propertyState->intValue[0] = value; propertyState->intValue[0] = value;
propertyState->serialized = true; propertyState->serialized = true;
return true; changed = true;
}
break;
} }
case ::XCEngine::Resources::ShaderPropertyType::Color: case ::XCEngine::Resources::ShaderPropertyType::Color:
if (!UI::DrawPropertyColor4(label, propertyState->floatValue.data())) { if (UI::DrawPropertyColor4(label, propertyState->floatValue.data())) {
return false;
}
propertyState->serialized = true; propertyState->serialized = true;
return true; changed = true;
}
break;
case ::XCEngine::Resources::ShaderPropertyType::Vector: { case ::XCEngine::Resources::ShaderPropertyType::Vector: {
if (propertyState->type == ::XCEngine::Resources::MaterialPropertyType::Float2) { if (propertyState->type == ::XCEngine::Resources::MaterialPropertyType::Float2) {
::XCEngine::Math::Vector2 value(propertyState->floatValue[0], propertyState->floatValue[1]); ::XCEngine::Math::Vector2 value(propertyState->floatValue[0], propertyState->floatValue[1]);
if (!UI::DrawPropertyVec2(label, value)) { if (UI::DrawPropertyVec2(label, value)) {
return false;
}
propertyState->floatValue[0] = value.x; propertyState->floatValue[0] = value.x;
propertyState->floatValue[1] = value.y; propertyState->floatValue[1] = value.y;
propertyState->serialized = true; propertyState->serialized = true;
return true; changed = true;
}
break;
} }
if (propertyState->type == ::XCEngine::Resources::MaterialPropertyType::Float3) { if (propertyState->type == ::XCEngine::Resources::MaterialPropertyType::Float3) {
::XCEngine::Math::Vector3 value( ::XCEngine::Math::Vector3 value(
propertyState->floatValue[0], propertyState->floatValue[0],
propertyState->floatValue[1], propertyState->floatValue[1],
propertyState->floatValue[2]); propertyState->floatValue[2]);
if (!UI::DrawPropertyVec3(label, value)) { if (UI::DrawPropertyVec3(label, value)) {
return false;
}
propertyState->floatValue[0] = value.x; propertyState->floatValue[0] = value.x;
propertyState->floatValue[1] = value.y; propertyState->floatValue[1] = value.y;
propertyState->floatValue[2] = value.z; propertyState->floatValue[2] = value.z;
propertyState->serialized = true; propertyState->serialized = true;
return true; changed = true;
} }
if (!DrawFloat4Property(label, propertyState->floatValue.data())) { break;
return false;
} }
if (DrawFloat4Property(label, propertyState->floatValue.data())) {
propertyState->serialized = true; propertyState->serialized = true;
return true; changed = true;
}
break;
} }
case ::XCEngine::Resources::ShaderPropertyType::Texture2D: case ::XCEngine::Resources::ShaderPropertyType::Texture2D:
@@ -672,25 +674,39 @@ bool DrawShaderDrivenMaterialProperty(
"Select Texture Asset", "Select Texture Asset",
{ ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds", ".hdr", ".ppm" }); { ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds", ".hdr", ".ppm" });
if (textureInteraction.clearRequested) { if (textureInteraction.clearRequested) {
if (propertyState->texturePath.empty()) { if (!propertyState->texturePath.empty()) {
return false;
}
propertyState->texturePath.clear(); propertyState->texturePath.clear();
propertyState->serialized = false; propertyState->serialized = false;
return true; changed = true;
} }
if (!textureInteraction.assignedPath.empty() && } else if (!textureInteraction.assignedPath.empty() &&
textureInteraction.assignedPath != propertyState->texturePath) { textureInteraction.assignedPath != propertyState->texturePath) {
propertyState->texturePath = textureInteraction.assignedPath; propertyState->texturePath = textureInteraction.assignedPath;
propertyState->serialized = true; propertyState->serialized = true;
return true; changed = true;
} }
return false; break;
} }
default: default:
return false; 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) { 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) { for (Core::uint32 keywordIndex = 0; keywordIndex < material.GetKeywordCount(); ++keywordIndex) {
MaterialKeywordState keywordState; MaterialKeywordState keywordState;
keywordState.value = std::string(material.GetKeyword(keywordIndex).CStr()); keywordState.value = std::string(material.GetKeyword(keywordIndex).CStr());
keywordState.serialized = true;
m_materialAssetState.keywords.push_back(std::move(keywordState)); m_materialAssetState.keywords.push_back(std::move(keywordState));
} }
m_materialAssetState.properties = CollectMaterialPropertyStates(material); 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); const std::string sourceText = ReadTextFileOrEmpty(m_materialAssetState.assetFullPath);
if (!sourceText.empty()) { if (!sourceText.empty()) {
@@ -1234,7 +1256,9 @@ void InspectorPanel::RenderMaterialAsset() {
} else { } else {
bool changed = false; bool changed = false;
for (const ::XCEngine::Resources::ShaderPropertyDesc& shaderProperty : materialShaderHandle->GetProperties()) { 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()) { if (!shaderProperty.defaultValue.Empty()) {
const std::string defaultText = const std::string defaultText =
std::string("Default: ") + shaderProperty.defaultValue.CStr(); std::string("Default: ") + shaderProperty.defaultValue.CStr();
@@ -1251,6 +1275,35 @@ void InspectorPanel::RenderMaterialAsset() {
UI::EndComponentSection(propertiesSection); 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( const UI::ComponentSectionResult renderStateSection = UI::BeginComponentSection(
"MaterialAssetRenderState", "MaterialAssetRenderState",
"Render State"); "Render State");

View File

@@ -423,6 +423,52 @@ std::vector<MaterialPropertyState> BuildShaderDefaultPropertyStates(
return CollectMaterialPropertyStates(scratchMaterial); return CollectMaterialPropertyStates(scratchMaterial);
} }
std::vector<MaterialKeywordState> BuildShaderKeywordStates(
const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle,
const std::vector<MaterialKeywordState>& previousKeywords) {
if (!shaderHandle.IsValid() || shaderHandle.Get() == nullptr) {
return {};
}
std::unordered_map<std::string, MaterialKeywordState> 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<MaterialKeywordState> keywordStates;
std::unordered_set<std::string> 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; constexpr int kCustomRenderQueuePresetIndex = 5;
int ResolveRenderQueuePresetIndex(int renderQueue) { int ResolveRenderQueuePresetIndex(int renderQueue) {
@@ -652,16 +698,40 @@ void SyncMaterialAssetStateWithShader(
CopyMaterialPropertyValue(previousPropertyIt->second, property); CopyMaterialPropertyValue(previousPropertyIt->second, property);
} }
state.properties = std::move(nextProperties); state.properties = std::move(nextProperties);
state.keywords = BuildShaderKeywordStates(shaderHandle, state.keywords);
}
std::vector<MaterialKeywordState> nextKeywords; bool ResetMaterialPropertyStateToShaderDefault(
nextKeywords.reserve(state.keywords.size()); const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle,
for (const MaterialKeywordState& keyword : state.keywords) { const std::string& propertyName,
const std::string keywordValue = TrimCopy(keyword.value); MaterialAssetState& state) {
if (!keywordValue.empty() && shaderHandle->DeclaresKeyword(keywordValue.c_str())) { if (!shaderHandle.IsValid() || shaderHandle.Get() == nullptr || propertyName.empty()) {
nextKeywords.push_back(keyword); 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<MaterialPropertyState> 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( void ApplyMaterialAuthoringPresenceToState(

View File

@@ -24,6 +24,11 @@ void SyncMaterialAssetStateWithShader(
const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle, const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle,
MaterialAssetState& state); MaterialAssetState& state);
bool ResetMaterialPropertyStateToShaderDefault(
const ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& shaderHandle,
const std::string& propertyName,
MaterialAssetState& state);
void ApplyMaterialAuthoringPresenceToState( void ApplyMaterialAuthoringPresenceToState(
const std::string& sourceText, const std::string& sourceText,
MaterialAssetState& state); MaterialAssetState& state);

View File

@@ -22,6 +22,7 @@ using XCEngine::Editor::MaterialAssetState;
using XCEngine::Editor::MaterialKeywordState; using XCEngine::Editor::MaterialKeywordState;
using XCEngine::Editor::MaterialPropertyState; using XCEngine::Editor::MaterialPropertyState;
using XCEngine::Editor::MaterialTagEditRow; using XCEngine::Editor::MaterialTagEditRow;
using XCEngine::Editor::ResetMaterialPropertyStateToShaderDefault;
using XCEngine::Editor::SyncMaterialAssetStateWithShader; using XCEngine::Editor::SyncMaterialAssetStateWithShader;
using XCEngine::Resources::IResource; using XCEngine::Resources::IResource;
using XCEngine::Resources::MaterialBlendFactor; using XCEngine::Resources::MaterialBlendFactor;
@@ -219,6 +220,41 @@ TEST(MaterialInspectorMaterialStateIOTest, SyncMaterialAssetStateWithShaderPrese
EXPECT_TRUE(state.keywords[0].serialized); EXPECT_TRUE(state.keywords[0].serialized);
} }
TEST(MaterialInspectorMaterialStateIOTest, SyncMaterialAssetStateWithShaderAddsDeclaredKeywordsAsDisabledEntries) {
auto shader = CreateSchemaShader(
"memory://material_inspector_keywords_shader",
{ "XC_ALPHA_TEST", "XC_DETAIL" });
ResourceHandle<Shader> 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<Shader> 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) { TEST(MaterialInspectorMaterialStateIOTest, BuildMaterialAssetFileTextOmitsNonSerializedDefaultsAndDisabledRenderState) {
MaterialAssetState state; MaterialAssetState state;
CopyText("Assets/Shaders/test.shader", state.shaderPath); CopyText("Assets/Shaders/test.shader", state.shaderPath);