From 4c26b410cb1dac618a27ef7e42eb6fdf3d77e657 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 7 Apr 2026 21:42:08 +0800 Subject: [PATCH] editor: add shader-driven material property inspector --- ...ctor与Shader属性面板收口计划_2026-04-07.md | 41 +++ editor/src/panels/InspectorPanel.cpp | 243 ++++++++++++++++++ 2 files changed, 284 insertions(+) diff --git a/docs/plan/Material Inspector与Shader属性面板收口计划_2026-04-07.md b/docs/plan/Material Inspector与Shader属性面板收口计划_2026-04-07.md index 4b21822b..0873a6f8 100644 --- a/docs/plan/Material Inspector与Shader属性面板收口计划_2026-04-07.md +++ b/docs/plan/Material Inspector与Shader属性面板收口计划_2026-04-07.md @@ -440,3 +440,44 @@ Material 面板不应再次演化成随意堆字段的临时入口。所有 rend - 关键词与属性在 Inspector 中的可视化编辑 因此,Phase 2 的性质是“先把材质状态模型与保存链路做对”,而不是“材质面板功能已经完整”。 + +## 13. Phase 3 执行结果 + +状态:已完成 + +本阶段已经开始把正确的材质状态模型真正暴露到 Inspector 上,重点是基于 Shader schema 生成属性面板,并处理 Shader 切换时的状态重建。 + +### 13.1 已完成内容 + +- Inspector 已新增 `Properties` 区块。 +- 属性区会基于当前 Shader 的 schema 动态生成,而不是写死字段。 +- 当前已接入的属性类型包括: + - `Float / Range` + - `Int` + - `Color` + - `Vector` + - `Texture2D / TextureCube` +- Texture 类型已接入资源选择控件,不再只是文本占位。 +- 每个属性当前会直接显示 Shader 中声明的默认值文本,作为当前参数的基线提示。 + +### 13.2 Shader 切换行为已收口 + +本阶段同时处理了一个关键一致性问题: + +- 当用户切换 Shader 时,Inspector 会先基于新 Shader schema 重建 `MaterialAssetState` +- 能与新 Shader 对齐的同名属性会尽量保留原值 +- 已不被新 Shader 声明的旧属性不会继续残留在保存结果里 +- 与新 Shader 不匹配的旧关键词也会被清理 + +这样可以避免旧材质属性被继续写回到新 Shader 材质文件中,从而减少生成无效材质源文件的风险。 + +### 13.3 本阶段仍未完成的部分 + +以下内容还需要后续阶段继续收口: + +- 属性默认值与显式覆盖值的正式“重置/回退”交互 +- 关键词的可视化编辑 UI +- 更完整的属性类型与显示策略细化 +- 针对 Inspector 材质链路的专门自动化测试 + +因此,Phase 3 的完成标准是“Shader schema 驱动的属性面板已经建立起来”,但还不是最终形态。 diff --git a/editor/src/panels/InspectorPanel.cpp b/editor/src/panels/InspectorPanel.cpp index d21a5a4c..bca286cd 100644 --- a/editor/src/panels/InspectorPanel.cpp +++ b/editor/src/panels/InspectorPanel.cpp @@ -726,6 +726,194 @@ bool TryGetLoadedShaderHandle( return outShader.IsValid(); } +bool TryResolveShaderHandle( + const std::string& shaderPath, + ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader>& outShader) { + if (TryGetLoadedShaderHandle(shaderPath, outShader)) { + return true; + } + + outShader.Reset(); + if (shaderPath.empty()) { + return false; + } + + outShader = ::XCEngine::Resources::ResourceManager::Get().Load<::XCEngine::Resources::Shader>(shaderPath.c_str()); + 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; +} + +std::vector 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 previousProperties; + previousProperties.reserve(state.properties.size()); + for (const InspectorPanel::MaterialPropertyState& property : state.properties) { + if (!property.name.empty()) { + previousProperties.emplace(property.name, property); + } + } + + std::vector 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 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); +} + +InspectorPanel::MaterialPropertyState* FindMaterialPropertyState( + InspectorPanel::MaterialAssetState& state, + const Containers::String& propertyName) { + const std::string propertyNameText(propertyName.CStr()); + for (InspectorPanel::MaterialPropertyState& property : state.properties) { + if (property.name == propertyNameText) { + return &property; + } + } + + return nullptr; +} + +bool DrawFloat4Property(const char* label, float values[4]) { + return UI::DrawPropertyRow(label, UI::InspectorPropertyLayout(), [&](const UI::PropertyLayoutMetrics& layout) { + UI::AlignPropertyControlVertically(layout, ImGui::GetFrameHeight()); + ImGui::PushID(label); + ImGui::SetNextItemWidth(layout.controlWidth); + const bool changed = ImGui::InputFloat4("##Value", values, "%.3f"); + ImGui::PopID(); + return changed; + }); +} + +bool DrawShaderDrivenMaterialProperty( + const ::XCEngine::Resources::ShaderPropertyDesc& shaderProperty, + InspectorPanel::MaterialAssetState& state) { + InspectorPanel::MaterialPropertyState* propertyState = FindMaterialPropertyState(state, shaderProperty.name); + if (propertyState == nullptr) { + return false; + } + + const char* label = shaderProperty.displayName.Empty() + ? shaderProperty.name.CStr() + : shaderProperty.displayName.CStr(); + + switch (shaderProperty.type) { + case ::XCEngine::Resources::ShaderPropertyType::Float: + case ::XCEngine::Resources::ShaderPropertyType::Range: + return UI::DrawPropertyFloat(label, propertyState->floatValue[0], 0.01f); + + case ::XCEngine::Resources::ShaderPropertyType::Int: { + int value = propertyState->intValue[0]; + if (!UI::DrawPropertyInt(label, value, 1)) { + return false; + } + propertyState->intValue[0] = value; + return true; + } + + case ::XCEngine::Resources::ShaderPropertyType::Color: + return UI::DrawPropertyColor4(label, propertyState->floatValue.data()); + + 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; + } + propertyState->floatValue[0] = value.x; + propertyState->floatValue[1] = value.y; + return true; + } + 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; + } + propertyState->floatValue[0] = value.x; + propertyState->floatValue[1] = value.y; + propertyState->floatValue[2] = value.z; + return true; + } + return DrawFloat4Property(label, propertyState->floatValue.data()); + } + + case ::XCEngine::Resources::ShaderPropertyType::Texture2D: + case ::XCEngine::Resources::ShaderPropertyType::TextureCube: { + const InspectorAssetReferenceInteraction textureInteraction = + DrawInspectorAssetReferenceProperty( + label, + propertyState->texturePath, + "Select Texture Asset", + { ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds", ".hdr", ".ppm" }); + if (textureInteraction.clearRequested) { + if (propertyState->texturePath.empty()) { + return false; + } + propertyState->texturePath.clear(); + return true; + } + if (!textureInteraction.assignedPath.empty() && + textureInteraction.assignedPath != propertyState->texturePath) { + propertyState->texturePath = textureInteraction.assignedPath; + return true; + } + return false; + } + + default: + return false; + } +} + std::string BuildMaterialLoadFailureMessage(const ::XCEngine::Resources::LoadResult& result) { if (!result.errorMessage.Empty()) { return std::string("Material file is invalid or unavailable: ") + result.errorMessage.CStr(); @@ -1068,6 +1256,7 @@ void InspectorPanel::ApplyMaterialAssetStateToSelectedMaterial() { ++m_materialShaderLoadRevision; m_materialShaderLoadInFlight = false; m_pendingMaterialShaderPath.clear(); + SyncMaterialAssetStateWithShader(shaderHandle, m_materialAssetState); ApplyResolvedMaterialStateToResource(m_materialAssetState, shaderHandle, material); m_materialAssetState.errorMessage.clear(); return; @@ -1117,6 +1306,7 @@ void InspectorPanel::ApplyMaterialAssetStateToSelectedMaterial() { return; } + SyncMaterialAssetStateWithShader(loadedShader, m_materialAssetState); ApplyResolvedMaterialStateToResource( m_materialAssetState, loadedShader, @@ -1325,6 +1515,15 @@ bool InspectorPanel::SaveMaterialAsset() { } try { + ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader> shaderHandle; + const std::string shaderPath = TrimCopy(BufferToString(m_materialAssetState.shaderPath)); + if (!shaderPath.empty() && TryResolveShaderHandle(shaderPath, shaderHandle)) { + SyncMaterialAssetStateWithShader(shaderHandle, m_materialAssetState); + } else if (shaderPath.empty()) { + m_materialAssetState.keywords.clear(); + m_materialAssetState.properties.clear(); + } + const std::filesystem::path materialPath(Platform::Utf8ToWide(m_materialAssetState.assetFullPath)); std::ofstream output(materialPath, std::ios::out | std::ios::trunc); if (!output.is_open()) { @@ -1389,10 +1588,16 @@ void InspectorPanel::RenderMaterialAsset() { { ".shader", ".xcshader" }); if (shaderInteraction.clearRequested) { CopyToCharBuffer(std::string(), m_materialAssetState.shaderPath); + m_materialAssetState.keywords.clear(); + m_materialAssetState.properties.clear(); changed = true; } else if (!shaderInteraction.assignedPath.empty() && shaderInteraction.assignedPath != BufferToString(m_materialAssetState.shaderPath)) { CopyToCharBuffer(shaderInteraction.assignedPath, m_materialAssetState.shaderPath); + ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader> shaderHandle; + if (TryResolveShaderHandle(shaderInteraction.assignedPath, shaderHandle)) { + SyncMaterialAssetStateWithShader(shaderHandle, m_materialAssetState); + } changed = true; } @@ -1421,6 +1626,44 @@ void InspectorPanel::RenderMaterialAsset() { UI::EndComponentSection(materialSection); } + ::XCEngine::Resources::ResourceHandle<::XCEngine::Resources::Shader> materialShaderHandle; + const std::string materialShaderPath = TrimCopy(BufferToString(m_materialAssetState.shaderPath)); + const bool hasMaterialShader = + !materialShaderPath.empty() && + TryResolveShaderHandle(materialShaderPath, materialShaderHandle) && + materialShaderHandle.IsValid() && + materialShaderHandle.Get() != nullptr; + + const UI::ComponentSectionResult propertiesSection = UI::BeginComponentSection( + "MaterialAssetProperties", + "Properties"); + if (propertiesSection.open) { + ImGui::Indent(propertiesSection.contentIndent); + + if (!hasMaterialShader) { + UI::DrawHintText("Select a shader to expose material properties."); + } else if (materialShaderHandle->GetProperties().Empty()) { + UI::DrawHintText("Selected shader does not expose editable properties."); + } else { + bool changed = false; + for (const ::XCEngine::Resources::ShaderPropertyDesc& shaderProperty : materialShaderHandle->GetProperties()) { + changed = DrawShaderDrivenMaterialProperty(shaderProperty, m_materialAssetState) || changed; + if (!shaderProperty.defaultValue.Empty()) { + const std::string defaultText = + std::string("Default: ") + shaderProperty.defaultValue.CStr(); + UI::DrawHintText(defaultText.c_str()); + } + } + + if (changed) { + m_materialAssetState.dirty = true; + SaveMaterialAsset(); + } + } + + UI::EndComponentSection(propertiesSection); + } + const UI::ComponentSectionResult renderStateSection = UI::BeginComponentSection( "MaterialAssetRenderState", "Render State");