editor: add shader-driven material property inspector
This commit is contained in:
@@ -440,3 +440,44 @@ Material 面板不应再次演化成随意堆字段的临时入口。所有 rend
|
|||||||
- 关键词与属性在 Inspector 中的可视化编辑
|
- 关键词与属性在 Inspector 中的可视化编辑
|
||||||
|
|
||||||
因此,Phase 2 的性质是“先把材质状态模型与保存链路做对”,而不是“材质面板功能已经完整”。
|
因此,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 驱动的属性面板已经建立起来”,但还不是最终形态。
|
||||||
|
|||||||
@@ -726,6 +726,194 @@ bool TryGetLoadedShaderHandle(
|
|||||||
return outShader.IsValid();
|
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<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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
std::string BuildMaterialLoadFailureMessage(const ::XCEngine::Resources::LoadResult& result) {
|
||||||
if (!result.errorMessage.Empty()) {
|
if (!result.errorMessage.Empty()) {
|
||||||
return std::string("Material file is invalid or unavailable: ") + result.errorMessage.CStr();
|
return std::string("Material file is invalid or unavailable: ") + result.errorMessage.CStr();
|
||||||
@@ -1068,6 +1256,7 @@ void InspectorPanel::ApplyMaterialAssetStateToSelectedMaterial() {
|
|||||||
++m_materialShaderLoadRevision;
|
++m_materialShaderLoadRevision;
|
||||||
m_materialShaderLoadInFlight = false;
|
m_materialShaderLoadInFlight = false;
|
||||||
m_pendingMaterialShaderPath.clear();
|
m_pendingMaterialShaderPath.clear();
|
||||||
|
SyncMaterialAssetStateWithShader(shaderHandle, m_materialAssetState);
|
||||||
ApplyResolvedMaterialStateToResource(m_materialAssetState, shaderHandle, material);
|
ApplyResolvedMaterialStateToResource(m_materialAssetState, shaderHandle, material);
|
||||||
m_materialAssetState.errorMessage.clear();
|
m_materialAssetState.errorMessage.clear();
|
||||||
return;
|
return;
|
||||||
@@ -1117,6 +1306,7 @@ void InspectorPanel::ApplyMaterialAssetStateToSelectedMaterial() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SyncMaterialAssetStateWithShader(loadedShader, m_materialAssetState);
|
||||||
ApplyResolvedMaterialStateToResource(
|
ApplyResolvedMaterialStateToResource(
|
||||||
m_materialAssetState,
|
m_materialAssetState,
|
||||||
loadedShader,
|
loadedShader,
|
||||||
@@ -1325,6 +1515,15 @@ bool InspectorPanel::SaveMaterialAsset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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));
|
const std::filesystem::path materialPath(Platform::Utf8ToWide(m_materialAssetState.assetFullPath));
|
||||||
std::ofstream output(materialPath, std::ios::out | std::ios::trunc);
|
std::ofstream output(materialPath, std::ios::out | std::ios::trunc);
|
||||||
if (!output.is_open()) {
|
if (!output.is_open()) {
|
||||||
@@ -1389,10 +1588,16 @@ void InspectorPanel::RenderMaterialAsset() {
|
|||||||
{ ".shader", ".xcshader" });
|
{ ".shader", ".xcshader" });
|
||||||
if (shaderInteraction.clearRequested) {
|
if (shaderInteraction.clearRequested) {
|
||||||
CopyToCharBuffer(std::string(), m_materialAssetState.shaderPath);
|
CopyToCharBuffer(std::string(), m_materialAssetState.shaderPath);
|
||||||
|
m_materialAssetState.keywords.clear();
|
||||||
|
m_materialAssetState.properties.clear();
|
||||||
changed = true;
|
changed = true;
|
||||||
} else if (!shaderInteraction.assignedPath.empty() &&
|
} else if (!shaderInteraction.assignedPath.empty() &&
|
||||||
shaderInteraction.assignedPath != BufferToString(m_materialAssetState.shaderPath)) {
|
shaderInteraction.assignedPath != BufferToString(m_materialAssetState.shaderPath)) {
|
||||||
CopyToCharBuffer(shaderInteraction.assignedPath, 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;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1421,6 +1626,44 @@ void InspectorPanel::RenderMaterialAsset() {
|
|||||||
UI::EndComponentSection(materialSection);
|
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(
|
const UI::ComponentSectionResult renderStateSection = UI::BeginComponentSection(
|
||||||
"MaterialAssetRenderState",
|
"MaterialAssetRenderState",
|
||||||
"Render State");
|
"Render State");
|
||||||
|
|||||||
Reference in New Issue
Block a user