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 <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/usdShade/material.h>
|
||||
#include <pxr/usd/usdShade/shader.h>
|
||||
@@ -9,10 +11,84 @@
|
||||
#include <pxr/base/tf/token.h>
|
||||
#include <pxr/base/vt/value.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <set>
|
||||
|
||||
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() {
|
||||
if (m_shaderNodeCacheBuilt)
|
||||
return m_shaderNodeCache;
|
||||
@@ -24,12 +100,19 @@ const std::vector<ShaderNodeTypeInfo>& MaterialManager::GetAvailableShaderNodes(
|
||||
info.identifier = node->GetIdentifier().GetString();
|
||||
info.label = !node->GetLabel().IsEmpty() ? node->GetLabel().GetString() : node->GetName();
|
||||
info.family = node->GetFamily().GetString();
|
||||
info.category = DeriveCategory(node);
|
||||
info.source = DeriveSource(node);
|
||||
m_shaderNodeCache.push_back(std::move(info));
|
||||
}
|
||||
|
||||
std::sort(m_shaderNodeCache.begin(), m_shaderNodeCache.end(),
|
||||
[](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;
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,15 @@ namespace UsdLayerManager {
|
||||
struct ShaderNodeTypeInfo {
|
||||
std::string identifier; ///< Sdr identifier, authored as info:id
|
||||
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
|
||||
|
||||
@@ -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
|
||||
// searchable create-node UIs (TAB popup, browser list); matches label or
|
||||
// identifier, and collapses duplicate labels (one per source-type parser
|
||||
// variant) to a single entry.
|
||||
// identifier, and collapses duplicate (source, label) pairs — parser
|
||||
// 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(
|
||||
const std::vector<ShaderNodeTypeInfo>& nodeTypes, const char* rawQuery) {
|
||||
auto toLower = [](std::string s) {
|
||||
@@ -140,13 +141,37 @@ std::vector<const ShaderNodeTypeInfo*> FilterShaderNodeTypes(
|
||||
toLower(info.label).find(query) == std::string::npos &&
|
||||
toLower(info.identifier).find(query) == std::string::npos)
|
||||
continue;
|
||||
if (!seenLabels.insert(info.label).second)
|
||||
if (!seenLabels.insert(info.source + "|" + info.label).second)
|
||||
continue;
|
||||
matches.push_back(&info);
|
||||
}
|
||||
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
|
||||
// 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
|
||||
@@ -231,19 +256,20 @@ void MaterialEditorPanel::Render() {
|
||||
|
||||
// Hypershade-style three-column layout: material browser on the left,
|
||||
// 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();
|
||||
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
|
||||
// (dot + name + value columns) fits below the shader ball.
|
||||
ImGui::BeginChild("MaterialCanvasRegion", ImVec2(-320.0f, 0.0f), false);
|
||||
ImGui::BeginChild("MaterialCanvasRegion", ImVec2(-(m_previewWidth + 6.0f), 0.0f), false);
|
||||
RenderNodeGraphCanvas();
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
VerticalSplitter("##MaterialSplitR", &m_previewWidth, 220.0f, 640.0f,
|
||||
/*rightSideColumn=*/true);
|
||||
|
||||
ImGui::BeginChild("MaterialPreviewRegion", ImVec2(0.0f, 0.0f), true);
|
||||
RenderPreviewPanel();
|
||||
@@ -633,21 +659,55 @@ void MaterialEditorPanel::RenderMaterialBrowser() {
|
||||
if (m_materialPath.IsEmpty()) {
|
||||
ImGui::TextDisabled("Open a material first");
|
||||
} else {
|
||||
const bool searching = (m_browserNodeSearchBuf[0] != '\0');
|
||||
const auto matches =
|
||||
FilterShaderNodeTypes(m_materialManager->GetAvailableShaderNodes(), m_browserNodeSearchBuf);
|
||||
if (matches.empty())
|
||||
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) {
|
||||
const ShaderNodeTypeInfo* info = matches[i];
|
||||
std::string itemLabel = info->label;
|
||||
if (!info->family.empty())
|
||||
itemLabel += " [" + info->family + "]";
|
||||
itemLabel += "##" + std::to_string(i);
|
||||
if (info->source != currentSource) {
|
||||
closeCategory();
|
||||
currentSource = info->source;
|
||||
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))
|
||||
m_pendingCreateShaderId = info->identifier; // created at view center next canvas pass
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", info->identifier.c_str());
|
||||
}
|
||||
closeCategory();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
@@ -1041,10 +1101,8 @@ void MaterialEditorPanel::RenderNodeSearchMenu(const ImVec2& canvasPos) {
|
||||
ImGui::TextDisabled("No matching nodes");
|
||||
for (int i = 0; i < static_cast<int>(matches.size()); ++i) {
|
||||
const ShaderNodeTypeInfo* info = matches[i];
|
||||
std::string itemLabel = info->label;
|
||||
if (!info->family.empty())
|
||||
itemLabel += " [" + info->family + "]";
|
||||
itemLabel += "##" + std::to_string(i);
|
||||
std::string itemLabel =
|
||||
info->label + " [" + info->source + "/" + info->category + "]##" + std::to_string(i);
|
||||
const bool selected = (i == m_nodeSearchSelected);
|
||||
if (ImGui::Selectable(itemLabel.c_str(), selected))
|
||||
chosen = info;
|
||||
|
||||
@@ -133,6 +133,11 @@ private:
|
||||
char m_nodeSearchBuf[128] = "";
|
||||
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.
|
||||
char m_browserNodeSearchBuf[128] = "";
|
||||
/// Shader id clicked in the browser's create-node list; created at the
|
||||
|
||||
Reference in New Issue
Block a user