Add Hypershade-style Material Editor with node graph and shader-ball preview

Browser column lists all scene materials plus a searchable create-node
list; Show Graph (or double-click) loads a material into the node-graph
work area. The graph shows the entire network including MaterialX
(.mtlx) node graphs, resolving connections through NodeGraph boundaries
via UsdShadeUtils::GetValueProducingAttributes, with layered auto-layout
for nodes lacking authored uiPosition. Canvas navigates viewport-style
(Alt+MMB pan / Alt+RMB zoom) and TAB opens a Nuke-style search popup.
Selecting a node shows a typed property editor (live-apply, one undo
command per edit) and previews that node's output on the shader ball.

The preview renders through its own Hydra engine into a scratch stage
that composes the material via a reference to the source root layer (so
referenced .mtlx materials work), re-renders until progressive delegates
(Arnold/Cycles/Embree) converge, and lights with HDR dome presets
(External/Room/Interior/Sunset; CC0 Poly Haven EXRs fetched at CMake
configure). texture:format is authored latlong explicitly - left
automatic, hdArnold falls back to Arnold's angular fisheye default -
and hdCycles gets a -90 X pole rotation to match Storm's +Y-pole
sampling.

Mutations go through new ICommand subclasses (create shader node,
connect/disconnect attrs). imgui-node-editor is vendored (gitignored)
with a local one-line patch: c_ScrollButtonIndex 1->2 for MMB pan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 20:44:54 +08:00
parent 338970b243
commit 09819091c4
20 changed files with 2366 additions and 4 deletions
+145
View File
@@ -0,0 +1,145 @@
#include "MaterialManager.h"
#include <pxr/usd/sdr/registry.h>
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/shader.h>
#include <pxr/usd/usdShade/nodeGraph.h>
#include <pxr/usd/usdShade/connectableAPI.h>
#include <pxr/usd/usdShade/utils.h>
#include <pxr/base/tf/token.h>
#include <pxr/base/vt/value.h>
#include <algorithm>
#include <set>
namespace UsdLayerManager {
const std::vector<ShaderNodeTypeInfo>& MaterialManager::GetAvailableShaderNodes() {
if (m_shaderNodeCacheBuilt)
return m_shaderNodeCache;
for (pxr::SdrShaderNodeConstPtr node : pxr::SdrRegistry::GetInstance().GetShaderNodesByFamily()) {
if (!node) continue;
ShaderNodeTypeInfo info;
info.identifier = node->GetIdentifier().GetString();
info.label = !node->GetLabel().IsEmpty() ? node->GetLabel().GetString() : node->GetName();
info.family = node->GetFamily().GetString();
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;
return a.label < b.label;
});
m_shaderNodeCacheBuilt = true;
return m_shaderNodeCache;
}
std::vector<pxr::SdfPath> MaterialManager::GetAllMaterials() const {
std::vector<pxr::SdfPath> result;
if (!m_stage) return result;
for (const pxr::UsdPrim& prim : m_stage->Traverse()) {
if (prim.IsA<pxr::UsdShadeMaterial>())
result.push_back(prim.GetPath());
}
return result;
}
ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& materialPath) const {
ShaderGraphSnapshot snapshot;
if (!m_stage) return snapshot;
pxr::UsdPrim materialPrim = m_stage->GetPrimAtPath(materialPath);
if (!materialPrim.IsValid()) return snapshot;
static const pxr::TfToken kUiPositionKey("uiPosition");
// Seed with every shader under the material, recursing into nested node
// graphs (usdMtlx nests a material's nodes inside UsdShadeNodeGraph
// scopes rather than authoring them as direct children).
std::vector<pxr::UsdPrim> pending;
std::vector<pxr::UsdPrim> scopes{materialPrim};
while (!scopes.empty()) {
pxr::UsdPrim scope = scopes.back();
scopes.pop_back();
for (const pxr::UsdPrim& child : scope.GetChildren()) {
if (child.IsA<pxr::UsdShadeShader>())
pending.push_back(child);
else if (child.IsA<pxr::UsdShadeNodeGraph>())
scopes.push_back(child);
}
}
// Worklist walk: resolving each input's true upstream shader output pulls
// in nodes living in node graphs outside the material prim too.
std::set<pxr::SdfPath> visited;
while (!pending.empty()) {
pxr::UsdPrim child = pending.back();
pending.pop_back();
if (!visited.insert(child.GetPath()).second)
continue;
pxr::UsdShadeShader shader(child);
if (!shader) continue;
ShaderGraphNode node;
node.path = child.GetPath();
pxr::TfToken shaderId;
shader.GetIdAttr().Get(&shaderId);
node.shaderId = shaderId.GetString();
pxr::VtValue posValue = child.GetCustomDataByKey(kUiPositionKey);
if (posValue.IsHolding<pxr::GfVec2f>()) {
node.uiPosition = posValue.UncheckedGet<pxr::GfVec2f>();
node.hasAuthoredPosition = true;
}
// Prefer the full Sdr-defined pin set (so unauthored pins can still be
// dragged to create a connection); fall back to authored attributes
// only for shader types the registry doesn't know about.
pxr::SdrShaderNodeConstPtr sdrNode =
pxr::SdrRegistry::GetInstance().GetShaderNodeByIdentifier(shaderId);
if (sdrNode) {
for (const pxr::TfToken& inputName : sdrNode->GetShaderInputNames()) {
if (auto* prop = sdrNode->GetShaderInput(inputName))
node.inputs.push_back({inputName.GetString(), prop->GetTypeAsSdfType().GetSdfType()});
}
for (const pxr::TfToken& outputName : sdrNode->GetShaderOutputNames()) {
if (auto* prop = sdrNode->GetShaderOutput(outputName))
node.outputs.push_back({outputName.GetString(), prop->GetTypeAsSdfType().GetSdfType()});
}
} else {
for (const auto& input : shader.GetInputs())
node.inputs.push_back({input.GetBaseName().GetString(), input.GetTypeName()});
for (const auto& output : shader.GetOutputs())
node.outputs.push_back({output.GetBaseName().GetString(), output.GetTypeName()});
}
// Only authored inputs can carry a connection. Resolve each one
// through node-graph boundaries (shaderOutputsOnly) so a MaterialX
// connection routed via NodeGraph interface attrs links straight to
// the shader output that actually produces the value.
for (const auto& input : shader.GetInputs()) {
for (const pxr::UsdAttribute& sourceAttr :
pxr::UsdShadeUtils::GetValueProducingAttributes(input, /*shaderOutputsOnly=*/true)) {
pxr::UsdPrim sourcePrim = sourceAttr.GetPrim();
if (!sourcePrim.IsA<pxr::UsdShadeShader>()) continue;
ShaderGraphLink link;
link.destNode = child.GetPath();
link.destInput = input.GetBaseName().GetString();
link.sourceNode = sourcePrim.GetPath();
link.sourceOutput = pxr::UsdShadeOutput(sourceAttr).GetBaseName().GetString();
snapshot.links.push_back(std::move(link));
pending.push_back(sourcePrim); // may live outside the material
}
}
snapshot.nodes.push_back(std::move(node));
}
return snapshot;
}
} // namespace UsdLayerManager
+84
View File
@@ -0,0 +1,84 @@
#pragma once
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/valueTypeName.h>
#include <pxr/base/gf/vec2f.h>
#include <string>
#include <vector>
namespace UsdLayerManager {
/// One entry in the shader-node creation menu, derived from the Sdr/Ndr
/// shader registry (covers UsdPreviewSurface, MaterialX, Arnold, Cycles,
/// etc. — whatever shader definitions are discoverable in this build).
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)
};
/// One input or output pin on a shader node, with its USD value type so
/// connections/CreateInput/CreateOutput can author the right type.
struct ShaderPinInfo {
std::string name;
pxr::SdfValueTypeName typeName;
};
/// One UsdShadeShader prim read back from an existing material network.
/// inputs/outputs list the full Sdr-defined pin set (not just authored
/// attributes) so unauthored pins can still be dragged to create a connection.
struct ShaderGraphNode {
pxr::SdfPath path;
std::string shaderId;
pxr::GfVec2f uiPosition{0.0f, 0.0f};
/// False when no uiPosition custom data is authored (typical for networks
/// referenced from .mtlx) — the editor auto-lays such nodes out instead.
bool hasAuthoredPosition = false;
std::vector<ShaderPinInfo> inputs;
std::vector<ShaderPinInfo> outputs;
};
/// One authored connection between two shader nodes in the same network.
struct ShaderGraphLink {
pxr::SdfPath destNode;
std::string destInput;
pxr::SdfPath sourceNode;
std::string sourceOutput;
};
struct ShaderGraphSnapshot {
std::vector<ShaderGraphNode> nodes;
std::vector<ShaderGraphLink> links;
};
/// Reads/introspects UsdShade material networks and the Sdr shader registry.
/// Mirrors PropertyManager's role: holds read/derive logic only — mutations
/// go through ICommand subclasses in src/core/commands/.
class MaterialManager {
public:
void SetStage(pxr::UsdStageRefPtr stage) { m_stage = stage; }
/// All shader node types registered in the Sdr registry, grouped for a
/// categorized create-node menu. Cached after the first (slow) call.
const std::vector<ShaderNodeTypeInfo>& GetAvailableShaderNodes();
/// The entire shader network of the material at materialPath: shaders
/// under the material (recursing into nested UsdShadeNodeGraphs) plus
/// everything reachable upstream through connections — including nodes in
/// node graphs outside the material, as authored by MaterialX (.mtlx)
/// imports. Connections are resolved through node-graph boundary attrs to
/// the shader outputs that actually produce the values.
ShaderGraphSnapshot GetShaderGraph(const pxr::SdfPath& materialPath) const;
/// Paths of every UsdShadeMaterial prim on the stage, in traversal order.
/// Drives the material browser list (Hypershade-style).
std::vector<pxr::SdfPath> GetAllMaterials() const;
private:
pxr::UsdStageRefPtr m_stage;
std::vector<ShaderNodeTypeInfo> m_shaderNodeCache;
bool m_shaderNodeCacheBuilt = false;
};
} // namespace UsdLayerManager
+1 -1
View File
@@ -705,7 +705,7 @@ void UsdSceneRenderer::Render(int width, int height)
// --- Render params (matches stageView.renderSinglePass) ---
m_renderParams = pxr::UsdImagingGLRenderParams();
m_renderParams.frame = m_currentTime;
m_renderParams.complexity = 1.0f;
m_renderParams.complexity = m_complexity;
// Apply shading mode → drawMode + enableLighting
switch (m_shadingMode) {
case ShadingMode::FlatShaded:
+12
View File
@@ -185,12 +185,23 @@ public:
/// unknown or the switch fails. The renderer is re-initialised if needed.
bool SetRendererPlugin(const pxr::TfToken& pluginId);
/// False while a progressive delegate (Arnold/Cycles/Embree…) is still
/// accumulating samples for the last-submitted frame; Storm is always
/// converged after one pass. Callers that cache the output texture must
/// keep calling Render() until this is true.
bool IsConverged() const { return !m_renderer || m_renderer->IsConverged(); }
// -----------------------------------------------------------------------
// View settings
// -----------------------------------------------------------------------
bool ShowGrid() const { return m_showGrid; }
void SetShowGrid(bool show) { m_showGrid = show; }
/// Hydra refinement complexity (1.0 = base mesh, up to 2.0). The shader
/// ball uses a raised value so the sphere tessellates smoothly.
float Complexity() const { return m_complexity; }
void SetComplexity(float c) { m_complexity = c; }
bool ShowCameraGuide() const { return m_showCameraGuide; }
void SetShowCameraGuide(bool show) { m_showCameraGuide = show; }
@@ -360,6 +371,7 @@ private:
std::vector<pxr::GfVec4d> m_clipPlanes;
bool m_showGrid;
float m_complexity = 1.0f;
bool m_showCameraGuide;
bool m_showGuides;
bool m_showProxy;
@@ -0,0 +1,82 @@
#include "ConnectShaderAttrsCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usdShade/shader.h>
#include <pxr/usd/usdShade/connectableAPI.h>
#include <pxr/base/tf/token.h>
namespace UsdLayerManager {
ConnectShaderAttrsCommand::ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& destNode,
const std::string& destInput,
const pxr::SdfValueTypeName& destType,
const pxr::SdfPath& sourceNode,
const std::string& sourceOutput,
const pxr::SdfValueTypeName& sourceType)
: m_stage(stage)
, m_destNode(destNode)
, m_destInput(destInput)
, m_destType(destType)
, m_sourceNode(sourceNode)
, m_sourceOutput(sourceOutput)
, m_sourceType(sourceType)
, m_description("Connect " + sourceNode.GetName() + "." + sourceOutput +
" -> " + destNode.GetName() + "." + destInput)
{
if (!stage) return;
pxr::UsdShadeShader destShader(stage->GetPrimAtPath(destNode));
if (!destShader) return;
pxr::UsdShadeInput existingInput = destShader.GetInput(pxr::TfToken(destInput));
if (!existingInput) return;
for (const auto& source : pxr::UsdShadeConnectableAPI::GetConnectedSources(existingInput)) {
if (!source.IsValid()) continue;
m_hadPriorConnection = true;
m_priorSourceNode = source.source.GetPrim().GetPath();
m_priorSourceOutput = source.sourceName.GetString();
break; // shader inputs carry a single upstream connection in this editor's model
}
}
void ConnectShaderAttrsCommand::Execute() {
if (!m_stage) return;
try {
pxr::UsdShadeShader destShader(m_stage->GetPrimAtPath(m_destNode));
pxr::UsdShadeShader sourceShader(m_stage->GetPrimAtPath(m_sourceNode));
if (!destShader || !sourceShader) {
LOG_ERROR("ConnectShaderAttrsCommand: invalid shader prim(s)");
return;
}
pxr::UsdShadeInput destInput = destShader.CreateInput(pxr::TfToken(m_destInput), m_destType);
pxr::UsdShadeOutput sourceOutput = sourceShader.CreateOutput(pxr::TfToken(m_sourceOutput), m_sourceType);
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, sourceOutput);
} catch (const std::exception& e) {
LOG_ERROR(std::string("ConnectShaderAttrsCommand::Execute error: ") + e.what());
}
}
void ConnectShaderAttrsCommand::Undo() {
if (!m_stage) return;
try {
pxr::UsdShadeShader destShader(m_stage->GetPrimAtPath(m_destNode));
if (!destShader) return;
pxr::UsdShadeInput destInput = destShader.GetInput(pxr::TfToken(m_destInput));
if (!destInput) return;
if (m_hadPriorConnection) {
pxr::UsdShadeShader priorSourceShader(m_stage->GetPrimAtPath(m_priorSourceNode));
pxr::UsdShadeOutput priorSourceOutput = priorSourceShader.GetOutput(pxr::TfToken(m_priorSourceOutput));
if (priorSourceShader && priorSourceOutput)
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, priorSourceOutput);
} else {
pxr::UsdShadeConnectableAPI::DisconnectSource(destInput);
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("ConnectShaderAttrsCommand::Undo error: ") + e.what());
}
}
} // namespace UsdLayerManager
@@ -0,0 +1,44 @@
#pragma once
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/valueTypeName.h>
#include <string>
namespace UsdLayerManager {
/// Connects a shader input to an upstream shader output, sparsely authoring
/// both attributes (with the given types) if they don't already exist.
/// Captures whatever the input was previously connected to (if anything) so
/// Undo can restore it exactly rather than merely disconnecting.
class ConnectShaderAttrsCommand : public ICommand {
public:
ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& destNode,
const std::string& destInput,
const pxr::SdfValueTypeName& destType,
const pxr::SdfPath& sourceNode,
const std::string& sourceOutput,
const pxr::SdfValueTypeName& sourceType);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_destNode;
std::string m_destInput;
pxr::SdfValueTypeName m_destType;
pxr::SdfPath m_sourceNode;
std::string m_sourceOutput;
pxr::SdfValueTypeName m_sourceType;
std::string m_description;
bool m_hadPriorConnection = false;
pxr::SdfPath m_priorSourceNode;
std::string m_priorSourceOutput;
};
} // namespace UsdLayerManager
@@ -0,0 +1,46 @@
#include "CreateShaderNodeCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usdShade/shader.h>
#include <pxr/base/tf/token.h>
#include <pxr/base/vt/value.h>
namespace UsdLayerManager {
CreateShaderNodeCommand::CreateShaderNodeCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& path,
const std::string& shaderId,
const pxr::GfVec2f& uiPosition)
: m_stage(stage)
, m_path(path)
, m_shaderId(shaderId)
, m_uiPosition(uiPosition)
, m_description("Create Shader " + shaderId + " " + path.GetString())
{
}
void CreateShaderNodeCommand::Execute() {
if (!m_stage) return;
try {
pxr::UsdShadeShader shader = pxr::UsdShadeShader::Define(m_stage, m_path);
if (!shader) {
LOG_ERROR("CreateShaderNodeCommand: failed to define shader " + m_path.GetString());
return;
}
shader.SetShaderId(pxr::TfToken(m_shaderId));
shader.GetPrim().SetCustomDataByKey(pxr::TfToken("uiPosition"), pxr::VtValue(m_uiPosition));
} catch (const std::exception& e) {
LOG_ERROR(std::string("CreateShaderNodeCommand::Execute error: ") + e.what());
}
}
void CreateShaderNodeCommand::Undo() {
if (!m_stage) return;
try {
if (!m_stage->RemovePrim(m_path))
LOG_ERROR("CreateShaderNodeCommand: failed to remove prim " + m_path.GetString());
} catch (const std::exception& e) {
LOG_ERROR(std::string("CreateShaderNodeCommand::Undo error: ") + e.what());
}
}
} // namespace UsdLayerManager
@@ -0,0 +1,34 @@
#pragma once
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/base/gf/vec2f.h>
#include <string>
namespace UsdLayerManager {
/// Creates a UsdShadeShader prim with the given Sdr shader identifier and
/// authors its canvas position as uiPosition custom data (see
/// docs/adr — Material Editor node-graph layout is stored as prim
/// customData, not scene data, so it never affects composition/renders).
class CreateShaderNodeCommand : public ICommand {
public:
CreateShaderNodeCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& path,
const std::string& shaderId,
const pxr::GfVec2f& uiPosition);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_path;
std::string m_shaderId;
pxr::GfVec2f m_uiPosition;
std::string m_description;
};
} // namespace UsdLayerManager
@@ -0,0 +1,63 @@
#include "DisconnectShaderAttrCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usdShade/shader.h>
#include <pxr/usd/usdShade/connectableAPI.h>
#include <pxr/base/tf/token.h>
namespace UsdLayerManager {
DisconnectShaderAttrCommand::DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& destNode,
const std::string& destInput)
: m_stage(stage)
, m_destNode(destNode)
, m_destInput(destInput)
, m_description("Disconnect " + destNode.GetName() + "." + destInput)
{
if (!stage) return;
pxr::UsdShadeShader destShader(stage->GetPrimAtPath(destNode));
if (!destShader) return;
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(destInput));
if (!input) return;
for (const auto& source : pxr::UsdShadeConnectableAPI::GetConnectedSources(input)) {
if (!source.IsValid()) continue;
m_hadConnection = true;
m_sourceNode = source.source.GetPrim().GetPath();
m_sourceOutput = source.sourceName.GetString();
break;
}
}
void DisconnectShaderAttrCommand::Execute() {
if (!m_stage) return;
try {
pxr::UsdShadeShader destShader(m_stage->GetPrimAtPath(m_destNode));
if (!destShader) return;
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput));
if (!input) return;
pxr::UsdShadeConnectableAPI::DisconnectSource(input);
} catch (const std::exception& e) {
LOG_ERROR(std::string("DisconnectShaderAttrCommand::Execute error: ") + e.what());
}
}
void DisconnectShaderAttrCommand::Undo() {
if (!m_stage || !m_hadConnection) return;
try {
pxr::UsdShadeShader destShader(m_stage->GetPrimAtPath(m_destNode));
pxr::UsdShadeShader sourceShader(m_stage->GetPrimAtPath(m_sourceNode));
if (!destShader || !sourceShader) return;
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput));
pxr::UsdShadeOutput output = sourceShader.GetOutput(pxr::TfToken(m_sourceOutput));
if (input && output)
pxr::UsdShadeConnectableAPI::ConnectToSource(input, output);
} catch (const std::exception& e) {
LOG_ERROR(std::string("DisconnectShaderAttrCommand::Undo error: ") + e.what());
}
}
} // namespace UsdLayerManager
@@ -0,0 +1,33 @@
#pragma once
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <string>
namespace UsdLayerManager {
/// Disconnects a shader input from its upstream source, capturing that
/// source at construction time so Undo can restore it.
class DisconnectShaderAttrCommand : public ICommand {
public:
DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& destNode,
const std::string& destInput);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_destNode;
std::string m_destInput;
std::string m_description;
bool m_hadConnection = false;
pxr::SdfPath m_sourceNode;
std::string m_sourceOutput;
};
} // namespace UsdLayerManager