Group create-node list by source and category; make editor columns resizable
The create-node list is now a two-level tree: a header per node source (USD, MaterialX, Arnold, Cycles - from the Sdr sourceType, with glslfx and OSL merged into USD so core nodes don't appear twice) containing Hypershade-style category sub-trees (Material / Texture / Geometry / Utility / Light) derived from Sdr context, terminal output types, role, and name keywords. Duplicate collapsing is per (source, label) so same-named nodes from different providers both stay; searching forces all sections open. The TAB popup shows [Source/Category] per entry. The browser and preview columns get drag splitters (viewport-divider styling, clamped widths); the canvas absorbs the remainder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
#include "MaterialManager.h"
|
#include "MaterialManager.h"
|
||||||
#include <pxr/usd/sdr/registry.h>
|
#include <pxr/usd/sdr/registry.h>
|
||||||
|
#include <pxr/usd/sdr/shaderProperty.h>
|
||||||
|
#include <pxr/base/tf/stringUtils.h>
|
||||||
#include <pxr/usd/usd/primRange.h>
|
#include <pxr/usd/usd/primRange.h>
|
||||||
#include <pxr/usd/usdShade/material.h>
|
#include <pxr/usd/usdShade/material.h>
|
||||||
#include <pxr/usd/usdShade/shader.h>
|
#include <pxr/usd/usdShade/shader.h>
|
||||||
@@ -9,10 +11,84 @@
|
|||||||
#include <pxr/base/tf/token.h>
|
#include <pxr/base/tf/token.h>
|
||||||
#include <pxr/base/vt/value.h>
|
#include <pxr/base/vt/value.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
namespace UsdLayerManager {
|
namespace UsdLayerManager {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Coarse Hypershade-style bucket for the create-node list. Sdr metadata is
|
||||||
|
// parser-dependent (glslfx vs MaterialX vs Arnold vs Cycles fill different
|
||||||
|
// fields), so classify from the strongest signal available: context first,
|
||||||
|
// then terminal output types, then role, then name keywords.
|
||||||
|
std::string DeriveCategory(pxr::SdrShaderNodeConstPtr node) {
|
||||||
|
const std::string context = pxr::TfStringify(node->GetContext());
|
||||||
|
if (context == "surface" || context == "volume" || context == "displacement")
|
||||||
|
return "Material";
|
||||||
|
if (context == "light" || context == "lightFilter")
|
||||||
|
return "Light";
|
||||||
|
|
||||||
|
// MaterialX/Arnold terminals often lack a context but type their outputs
|
||||||
|
// as surfaceshader/volumeshader/displacementshader/closure.
|
||||||
|
for (const pxr::TfToken& outName : node->GetShaderOutputNames()) {
|
||||||
|
if (const auto* prop = node->GetShaderOutput(outName)) {
|
||||||
|
const std::string outType = pxr::TfStringify(prop->GetType());
|
||||||
|
if (outType.find("shader") != std::string::npos ||
|
||||||
|
outType == "terminal" || outType == "closure")
|
||||||
|
return "Material";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string role = pxr::TfStringify(node->GetRole());
|
||||||
|
if (role == "texture") return "Texture";
|
||||||
|
if (role == "primvar") return "Geometry";
|
||||||
|
if (role == "math") return "Utility";
|
||||||
|
|
||||||
|
const std::string name = pxr::TfStringToLower(
|
||||||
|
node->GetIdentifier().GetString() + " " + node->GetName());
|
||||||
|
for (const char* kw : {"texture", "image", "checker", "noise", "ramp",
|
||||||
|
"fractal", "worley", "voronoi", "triplanar"})
|
||||||
|
if (name.find(kw) != std::string::npos) return "Texture";
|
||||||
|
for (const char* kw : {"primvar", "texcoord", "geomprop", "position",
|
||||||
|
"normal", "tangent", "bitangent", "uv_"})
|
||||||
|
if (name.find(kw) != std::string::npos) return "Geometry";
|
||||||
|
|
||||||
|
return "Utility";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed display order for the known buckets; anything unexpected sorts after.
|
||||||
|
int CategoryRank(const std::string& category) {
|
||||||
|
if (category == "Material") return 0;
|
||||||
|
if (category == "Texture") return 1;
|
||||||
|
if (category == "Geometry") return 2;
|
||||||
|
if (category == "Utility") return 3;
|
||||||
|
if (category == "Light") return 4;
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top-level group from the Sdr source type. glslfx and OSL collapse into one
|
||||||
|
// "USD" group — the core usd* nodes register under both parsers and would
|
||||||
|
// otherwise show up twice under different headers.
|
||||||
|
std::string DeriveSource(pxr::SdrShaderNodeConstPtr node) {
|
||||||
|
std::string st = pxr::TfStringToLower(pxr::TfStringify(node->GetSourceType()));
|
||||||
|
if (st == "glslfx" || st == "osl") return "USD";
|
||||||
|
if (st == "mtlx") return "MaterialX";
|
||||||
|
if (st == "arnold") return "Arnold";
|
||||||
|
if (st == "cycles") return "Cycles";
|
||||||
|
if (st.empty()) return "Other";
|
||||||
|
st[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(st[0])));
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SourceRank(const std::string& source) {
|
||||||
|
if (source == "USD") return 0;
|
||||||
|
if (source == "MaterialX") return 1;
|
||||||
|
return 2; // renderers and anything else, alphabetical among themselves
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
const std::vector<ShaderNodeTypeInfo>& MaterialManager::GetAvailableShaderNodes() {
|
const std::vector<ShaderNodeTypeInfo>& MaterialManager::GetAvailableShaderNodes() {
|
||||||
if (m_shaderNodeCacheBuilt)
|
if (m_shaderNodeCacheBuilt)
|
||||||
return m_shaderNodeCache;
|
return m_shaderNodeCache;
|
||||||
@@ -24,12 +100,19 @@ const std::vector<ShaderNodeTypeInfo>& MaterialManager::GetAvailableShaderNodes(
|
|||||||
info.identifier = node->GetIdentifier().GetString();
|
info.identifier = node->GetIdentifier().GetString();
|
||||||
info.label = !node->GetLabel().IsEmpty() ? node->GetLabel().GetString() : node->GetName();
|
info.label = !node->GetLabel().IsEmpty() ? node->GetLabel().GetString() : node->GetName();
|
||||||
info.family = node->GetFamily().GetString();
|
info.family = node->GetFamily().GetString();
|
||||||
|
info.category = DeriveCategory(node);
|
||||||
|
info.source = DeriveSource(node);
|
||||||
m_shaderNodeCache.push_back(std::move(info));
|
m_shaderNodeCache.push_back(std::move(info));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::sort(m_shaderNodeCache.begin(), m_shaderNodeCache.end(),
|
std::sort(m_shaderNodeCache.begin(), m_shaderNodeCache.end(),
|
||||||
[](const ShaderNodeTypeInfo& a, const ShaderNodeTypeInfo& b) {
|
[](const ShaderNodeTypeInfo& a, const ShaderNodeTypeInfo& b) {
|
||||||
if (a.family != b.family) return a.family < b.family;
|
const int sa = SourceRank(a.source), sb = SourceRank(b.source);
|
||||||
|
if (sa != sb) return sa < sb;
|
||||||
|
if (a.source != b.source) return a.source < b.source;
|
||||||
|
const int ra = CategoryRank(a.category), rb = CategoryRank(b.category);
|
||||||
|
if (ra != rb) return ra < rb;
|
||||||
|
if (a.category != b.category) return a.category < b.category;
|
||||||
return a.label < b.label;
|
return a.label < b.label;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,15 @@ namespace UsdLayerManager {
|
|||||||
struct ShaderNodeTypeInfo {
|
struct ShaderNodeTypeInfo {
|
||||||
std::string identifier; ///< Sdr identifier, authored as info:id
|
std::string identifier; ///< Sdr identifier, authored as info:id
|
||||||
std::string label; ///< display label for the create-node menu
|
std::string label; ///< display label for the create-node menu
|
||||||
std::string family; ///< grouping for the create-node menu (may be empty)
|
std::string family; ///< Sdr family (parser-provided, may be empty)
|
||||||
|
/// Coarse Hypershade-style bucket ("Material", "Texture", "Geometry",
|
||||||
|
/// "Utility", "Light") derived from Sdr context/role/output types; the
|
||||||
|
/// create-node list groups by it. Never empty.
|
||||||
|
std::string category;
|
||||||
|
/// Top-level create-list group derived from the Sdr source type:
|
||||||
|
/// "USD" (glslfx/OSL core nodes), "MaterialX", "Arnold", "Cycles", or a
|
||||||
|
/// capitalized raw source type. Never empty.
|
||||||
|
std::string source;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// One input or output pin on a shader node, with its USD value type so
|
/// One input or output pin on a shader node, with its USD value type so
|
||||||
|
|||||||
@@ -122,8 +122,9 @@ size_t ComputeGraphRevision(const pxr::UsdStageRefPtr& stage, const ShaderGraphS
|
|||||||
|
|
||||||
// Case-insensitive substring filter over the Sdr node-type list for the
|
// Case-insensitive substring filter over the Sdr node-type list for the
|
||||||
// searchable create-node UIs (TAB popup, browser list); matches label or
|
// searchable create-node UIs (TAB popup, browser list); matches label or
|
||||||
// identifier, and collapses duplicate labels (one per source-type parser
|
// identifier, and collapses duplicate (source, label) pairs — parser
|
||||||
// variant) to a single entry.
|
// variants within one source group — to a single entry. The same label under
|
||||||
|
// different sources (e.g. an "add" in both MaterialX and Arnold) stays.
|
||||||
std::vector<const ShaderNodeTypeInfo*> FilterShaderNodeTypes(
|
std::vector<const ShaderNodeTypeInfo*> FilterShaderNodeTypes(
|
||||||
const std::vector<ShaderNodeTypeInfo>& nodeTypes, const char* rawQuery) {
|
const std::vector<ShaderNodeTypeInfo>& nodeTypes, const char* rawQuery) {
|
||||||
auto toLower = [](std::string s) {
|
auto toLower = [](std::string s) {
|
||||||
@@ -140,13 +141,37 @@ std::vector<const ShaderNodeTypeInfo*> FilterShaderNodeTypes(
|
|||||||
toLower(info.label).find(query) == std::string::npos &&
|
toLower(info.label).find(query) == std::string::npos &&
|
||||||
toLower(info.identifier).find(query) == std::string::npos)
|
toLower(info.identifier).find(query) == std::string::npos)
|
||||||
continue;
|
continue;
|
||||||
if (!seenLabels.insert(info.label).second)
|
if (!seenLabels.insert(info.source + "|" + info.label).second)
|
||||||
continue;
|
continue;
|
||||||
matches.push_back(&info);
|
matches.push_back(&info);
|
||||||
}
|
}
|
||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vertical drag-splitter between the material editor's columns. Adjusts
|
||||||
|
// *width by the mouse drag (negated for a right-hand column, which grows
|
||||||
|
// when dragged left); colours match the viewport's split dividers.
|
||||||
|
void VerticalSplitter(const char* id, float* width, float minWidth, float maxWidth,
|
||||||
|
bool rightSideColumn) {
|
||||||
|
ImGui::SameLine(0.0f, 0.0f);
|
||||||
|
const float height = std::max(1.0f, ImGui::GetContentRegionAvail().y);
|
||||||
|
ImGui::InvisibleButton(id, ImVec2(6.0f, height));
|
||||||
|
const bool hovered = ImGui::IsItemHovered();
|
||||||
|
const bool active = ImGui::IsItemActive();
|
||||||
|
if (hovered || active)
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
|
||||||
|
if (active) {
|
||||||
|
const float delta = ImGui::GetIO().MouseDelta.x * (rightSideColumn ? -1.0f : 1.0f);
|
||||||
|
*width = std::clamp(*width + delta, minWidth, maxWidth);
|
||||||
|
}
|
||||||
|
ImVec2 rectMin = ImGui::GetItemRectMin();
|
||||||
|
ImVec2 rectMax = ImGui::GetItemRectMax();
|
||||||
|
ImGui::GetWindowDrawList()->AddRectFilled(
|
||||||
|
ImVec2(rectMin.x + 2.0f, rectMin.y), ImVec2(rectMax.x - 2.0f, rectMax.y),
|
||||||
|
(hovered || active) ? IM_COL32(250, 150, 66, 200) : IM_COL32(80, 80, 80, 180));
|
||||||
|
ImGui::SameLine(0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
// Networks referenced from .mtlx (or authored elsewhere) carry no uiPosition
|
// Networks referenced from .mtlx (or authored elsewhere) carry no uiPosition
|
||||||
// custom data, so every node would seed at (0,0) in a pile. Give those nodes
|
// custom data, so every node would seed at (0,0) in a pile. Give those nodes
|
||||||
// layered left-to-right positions instead: each node sits one column left of
|
// layered left-to-right positions instead: each node sits one column left of
|
||||||
@@ -231,19 +256,20 @@ void MaterialEditorPanel::Render() {
|
|||||||
|
|
||||||
// Hypershade-style three-column layout: material browser on the left,
|
// Hypershade-style three-column layout: material browser on the left,
|
||||||
// node-graph work area in the middle, shader-ball viewer on the right.
|
// node-graph work area in the middle, shader-ball viewer on the right.
|
||||||
ImGui::BeginChild("MaterialBrowserRegion", ImVec2(220.0f, 0.0f), true);
|
// The outer columns are user-resizable via the splitters between them.
|
||||||
|
ImGui::BeginChild("MaterialBrowserRegion", ImVec2(m_browserWidth, 0.0f), true);
|
||||||
RenderMaterialBrowser();
|
RenderMaterialBrowser();
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
|
||||||
ImGui::SameLine();
|
VerticalSplitter("##MaterialSplitL", &m_browserWidth, 140.0f, 500.0f,
|
||||||
|
/*rightSideColumn=*/false);
|
||||||
|
|
||||||
// Preview column is 320 wide so the PropertyPanel-style attribute table
|
ImGui::BeginChild("MaterialCanvasRegion", ImVec2(-(m_previewWidth + 6.0f), 0.0f), false);
|
||||||
// (dot + name + value columns) fits below the shader ball.
|
|
||||||
ImGui::BeginChild("MaterialCanvasRegion", ImVec2(-320.0f, 0.0f), false);
|
|
||||||
RenderNodeGraphCanvas();
|
RenderNodeGraphCanvas();
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
|
||||||
ImGui::SameLine();
|
VerticalSplitter("##MaterialSplitR", &m_previewWidth, 220.0f, 640.0f,
|
||||||
|
/*rightSideColumn=*/true);
|
||||||
|
|
||||||
ImGui::BeginChild("MaterialPreviewRegion", ImVec2(0.0f, 0.0f), true);
|
ImGui::BeginChild("MaterialPreviewRegion", ImVec2(0.0f, 0.0f), true);
|
||||||
RenderPreviewPanel();
|
RenderPreviewPanel();
|
||||||
@@ -633,21 +659,55 @@ void MaterialEditorPanel::RenderMaterialBrowser() {
|
|||||||
if (m_materialPath.IsEmpty()) {
|
if (m_materialPath.IsEmpty()) {
|
||||||
ImGui::TextDisabled("Open a material first");
|
ImGui::TextDisabled("Open a material first");
|
||||||
} else {
|
} else {
|
||||||
|
const bool searching = (m_browserNodeSearchBuf[0] != '\0');
|
||||||
const auto matches =
|
const auto matches =
|
||||||
FilterShaderNodeTypes(m_materialManager->GetAvailableShaderNodes(), m_browserNodeSearchBuf);
|
FilterShaderNodeTypes(m_materialManager->GetAvailableShaderNodes(), m_browserNodeSearchBuf);
|
||||||
if (matches.empty())
|
if (matches.empty())
|
||||||
ImGui::TextDisabled("No matching nodes");
|
ImGui::TextDisabled("No matching nodes");
|
||||||
|
// Matches arrive sorted by (source, category, label) — a collapsing
|
||||||
|
// header per source (USD / MaterialX / Arnold / ...) with a category
|
||||||
|
// sub-tree (Material / Texture / Geometry / Utility ...) inside,
|
||||||
|
// Hypershade-style. While searching, everything is forced open so a
|
||||||
|
// collapsed section can never hide a hit.
|
||||||
|
std::string currentSource;
|
||||||
|
std::string currentCategory;
|
||||||
|
bool sourceOpen = false;
|
||||||
|
bool categoryOpen = false;
|
||||||
|
auto closeCategory = [&]() {
|
||||||
|
if (categoryOpen) ImGui::TreePop();
|
||||||
|
categoryOpen = false;
|
||||||
|
};
|
||||||
for (int i = 0; i < static_cast<int>(matches.size()); ++i) {
|
for (int i = 0; i < static_cast<int>(matches.size()); ++i) {
|
||||||
const ShaderNodeTypeInfo* info = matches[i];
|
const ShaderNodeTypeInfo* info = matches[i];
|
||||||
std::string itemLabel = info->label;
|
if (info->source != currentSource) {
|
||||||
if (!info->family.empty())
|
closeCategory();
|
||||||
itemLabel += " [" + info->family + "]";
|
currentSource = info->source;
|
||||||
itemLabel += "##" + std::to_string(i);
|
currentCategory = std::string();
|
||||||
|
if (searching)
|
||||||
|
ImGui::SetNextItemOpen(true);
|
||||||
|
sourceOpen = ImGui::CollapsingHeader(currentSource.c_str(),
|
||||||
|
ImGuiTreeNodeFlags_DefaultOpen);
|
||||||
|
}
|
||||||
|
if (!sourceOpen)
|
||||||
|
continue;
|
||||||
|
if (info->category != currentCategory) {
|
||||||
|
closeCategory();
|
||||||
|
currentCategory = info->category;
|
||||||
|
if (searching)
|
||||||
|
ImGui::SetNextItemOpen(true);
|
||||||
|
// "##source" keeps IDs unique — category names repeat per source.
|
||||||
|
std::string catLabel = currentCategory + "##" + currentSource;
|
||||||
|
categoryOpen = ImGui::TreeNodeEx(catLabel.c_str(), ImGuiTreeNodeFlags_DefaultOpen);
|
||||||
|
}
|
||||||
|
if (!categoryOpen)
|
||||||
|
continue;
|
||||||
|
std::string itemLabel = info->label + "##" + std::to_string(i);
|
||||||
if (ImGui::Selectable(itemLabel.c_str(), false))
|
if (ImGui::Selectable(itemLabel.c_str(), false))
|
||||||
m_pendingCreateShaderId = info->identifier; // created at view center next canvas pass
|
m_pendingCreateShaderId = info->identifier; // created at view center next canvas pass
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", info->identifier.c_str());
|
ImGui::SetTooltip("%s", info->identifier.c_str());
|
||||||
}
|
}
|
||||||
|
closeCategory();
|
||||||
}
|
}
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
}
|
}
|
||||||
@@ -1041,10 +1101,8 @@ void MaterialEditorPanel::RenderNodeSearchMenu(const ImVec2& canvasPos) {
|
|||||||
ImGui::TextDisabled("No matching nodes");
|
ImGui::TextDisabled("No matching nodes");
|
||||||
for (int i = 0; i < static_cast<int>(matches.size()); ++i) {
|
for (int i = 0; i < static_cast<int>(matches.size()); ++i) {
|
||||||
const ShaderNodeTypeInfo* info = matches[i];
|
const ShaderNodeTypeInfo* info = matches[i];
|
||||||
std::string itemLabel = info->label;
|
std::string itemLabel =
|
||||||
if (!info->family.empty())
|
info->label + " [" + info->source + "/" + info->category + "]##" + std::to_string(i);
|
||||||
itemLabel += " [" + info->family + "]";
|
|
||||||
itemLabel += "##" + std::to_string(i);
|
|
||||||
const bool selected = (i == m_nodeSearchSelected);
|
const bool selected = (i == m_nodeSearchSelected);
|
||||||
if (ImGui::Selectable(itemLabel.c_str(), selected))
|
if (ImGui::Selectable(itemLabel.c_str(), selected))
|
||||||
chosen = info;
|
chosen = info;
|
||||||
|
|||||||
@@ -133,6 +133,11 @@ private:
|
|||||||
char m_nodeSearchBuf[128] = "";
|
char m_nodeSearchBuf[128] = "";
|
||||||
int m_nodeSearchSelected = 0;
|
int m_nodeSearchSelected = 0;
|
||||||
|
|
||||||
|
/// Column widths of the browser (left) and preview (right) sections,
|
||||||
|
/// user-adjustable via the splitters between them and the canvas.
|
||||||
|
float m_browserWidth = 220.0f;
|
||||||
|
float m_previewWidth = 320.0f;
|
||||||
|
|
||||||
/// Search text of the browser's persistent create-node list.
|
/// Search text of the browser's persistent create-node list.
|
||||||
char m_browserNodeSearchBuf[128] = "";
|
char m_browserNodeSearchBuf[128] = "";
|
||||||
/// Shader id clicked in the browser's create-node list; created at the
|
/// Shader id clicked in the browser's create-node list; created at the
|
||||||
|
|||||||
Reference in New Issue
Block a user