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
@@ -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