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:
@@ -46,6 +46,7 @@ Application::Application()
|
||||
, m_showPropertyPanel(true)
|
||||
, m_showTimeline(true)
|
||||
, m_showCurveEditor(false)
|
||||
, m_showMaterialEditor(false)
|
||||
, m_running(false) {
|
||||
}
|
||||
|
||||
@@ -84,6 +85,12 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
|
||||
m_curveEditorPanel = std::make_unique<CurveEditorPanel>();
|
||||
m_curveEditorPanel->SetCommandHistory(&m_commandHistory);
|
||||
|
||||
m_materialManager = std::make_unique<MaterialManager>();
|
||||
|
||||
m_materialEditorPanel = std::make_unique<MaterialEditorPanel>();
|
||||
m_materialEditorPanel->SetMaterialManager(m_materialManager.get());
|
||||
m_materialEditorPanel->SetCommandHistory(&m_commandHistory);
|
||||
|
||||
m_timelinePanel = std::make_unique<TimelinePanel>();
|
||||
m_timelinePanel->OnTimeChanged = [this](pxr::UsdTimeCode displayTime,
|
||||
pxr::UsdTimeCode editTime) {
|
||||
@@ -107,12 +114,14 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
|
||||
m_viewportPanel->SetIconManager(m_iconManager.get());
|
||||
m_timelinePanel->SetIconManager(m_iconManager.get());
|
||||
m_stageEditorPanel->SetIconManager(m_iconManager.get());
|
||||
m_materialEditorPanel->SetIconManager(m_iconManager.get());
|
||||
|
||||
m_sceneHierarchyPanel->SetOnPrimSelected(
|
||||
[this](const std::string& path) {
|
||||
m_viewportPanel->SetSelectedPrimPath(path);
|
||||
m_propertyPanel->SetSelectedPrimPath(path);
|
||||
m_curveEditorPanel->SetSelectedPrimPath(path);
|
||||
m_materialEditorPanel->SetTargetPrimPath(path);
|
||||
});
|
||||
|
||||
m_sceneHierarchyPanel->SetOnPrimsSelected(
|
||||
@@ -130,6 +139,7 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
|
||||
m_sceneHierarchyPanel->SetSelectedPath(path);
|
||||
m_propertyPanel->SetSelectedPrimPath(path);
|
||||
m_curveEditorPanel->SetSelectedPrimPath(path);
|
||||
m_materialEditorPanel->SetTargetPrimPath(path);
|
||||
};
|
||||
|
||||
// Rect drag in viewport → sync hierarchy + property panel + curve editor (primary)
|
||||
@@ -137,6 +147,14 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
|
||||
m_sceneHierarchyPanel->SetSelectedPaths(paths);
|
||||
m_propertyPanel->SetSelectedPrimPath(paths.empty() ? "" : paths.front());
|
||||
m_curveEditorPanel->SetSelectedPrimPath(paths.empty() ? "" : paths.front());
|
||||
m_materialEditorPanel->SetTargetPrimPath(paths.empty() ? "" : paths.front());
|
||||
};
|
||||
|
||||
// "Edit Material" in the Property Panel's Material Binding section →
|
||||
// open the Material Editor on the resolved material.
|
||||
m_propertyPanel->OnEditMaterialRequested = [this](const std::string& materialPath) {
|
||||
m_showMaterialEditor = true;
|
||||
m_materialEditorPanel->OpenOrCreateMaterial(materialPath);
|
||||
};
|
||||
|
||||
|
||||
@@ -173,6 +191,9 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
|
||||
m_viewportPanel->ApplyGlobalColorCorrection(
|
||||
m_prefs.colorCorrectionMode, m_prefs.ocioDisplay,
|
||||
m_prefs.ocioView, m_prefs.ocioColorSpace, m_prefs.ocioLook);
|
||||
m_materialEditorPanel->SetColorCorrectionFromPrefs(
|
||||
m_prefs.colorCorrectionMode, m_prefs.ocioDisplay,
|
||||
m_prefs.ocioView, m_prefs.ocioColorSpace, m_prefs.ocioLook);
|
||||
}
|
||||
|
||||
LOG_INFO("Application initialized successfully");
|
||||
@@ -201,6 +222,9 @@ void Application::Shutdown() {
|
||||
m_sceneHierarchyPanel.reset();
|
||||
m_propertyPanel.reset();
|
||||
m_stageEditorPanel.reset();
|
||||
// Owns the shader-ball preview's Hydra engine + GL draw target — must be
|
||||
// destroyed while the GL context still exists, like m_viewportPanel.
|
||||
m_materialEditorPanel.reset();
|
||||
m_propertyManager.reset();
|
||||
m_layerManager.reset();
|
||||
|
||||
@@ -233,6 +257,8 @@ void Application::RefreshManagers() {
|
||||
m_propertyPanel->SetStage(stage);
|
||||
m_timelinePanel->SetStage(stage);
|
||||
m_curveEditorPanel->SetStage(stage);
|
||||
m_materialManager->SetStage(stage);
|
||||
m_materialEditorPanel->SetStage(stage);
|
||||
} else {
|
||||
m_layerManager->SetStage(nullptr);
|
||||
m_propertyManager->SetStage(nullptr);
|
||||
@@ -241,6 +267,8 @@ void Application::RefreshManagers() {
|
||||
m_propertyPanel->SetStage(nullptr);
|
||||
m_timelinePanel->SetStage(nullptr);
|
||||
m_curveEditorPanel->SetStage(nullptr);
|
||||
m_materialManager->SetStage(nullptr);
|
||||
m_materialEditorPanel->SetStage(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,6 +346,13 @@ void Application::RenderUI() {
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (m_showMaterialEditor) {
|
||||
ImGui::SetNextWindowSize({960, 320}, ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin("Material Editor", &m_showMaterialEditor, ImGuiWindowFlags_NoCollapse);
|
||||
m_materialEditorPanel->Render();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
if (m_showPreferences)
|
||||
RenderPreferencesDialog();
|
||||
|
||||
@@ -414,6 +449,13 @@ void Application::ApplyPrefsToAllViewports()
|
||||
m_prefs.ocioView,
|
||||
m_prefs.ocioColorSpace,
|
||||
m_prefs.ocioLook);
|
||||
if (m_materialEditorPanel)
|
||||
m_materialEditorPanel->SetColorCorrectionFromPrefs(
|
||||
m_prefs.colorCorrectionMode,
|
||||
m_prefs.ocioDisplay,
|
||||
m_prefs.ocioView,
|
||||
m_prefs.ocioColorSpace,
|
||||
m_prefs.ocioLook);
|
||||
}
|
||||
|
||||
void Application::RenderPreferencesDialog()
|
||||
@@ -689,6 +731,7 @@ void Application::RenderMenuBar() {
|
||||
ImGui::MenuItem("Property Panel", nullptr, &m_showPropertyPanel);
|
||||
ImGui::MenuItem("Timeline", nullptr, &m_showTimeline);
|
||||
ImGui::MenuItem("Curve Editor", nullptr, &m_showCurveEditor);
|
||||
ImGui::MenuItem("Material Editor", nullptr, &m_showMaterialEditor);
|
||||
ImGui::Separator();
|
||||
ImGui::MenuItem("Stage Info", nullptr, &m_showStageInfo);
|
||||
ImGui::MenuItem("Demo Window", nullptr, &m_showDemoWindow);
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
#include "PropertyPanel.h"
|
||||
#include "TimelinePanel.h"
|
||||
#include "CurveEditorPanel.h"
|
||||
#include "MaterialEditorPanel.h"
|
||||
#include "../core/UsdStageManager.h"
|
||||
#include "../core/LayerManager.h"
|
||||
#include "../core/PropertyManager.h"
|
||||
#include "../core/MaterialManager.h"
|
||||
#include "../core/CommandHistory.h"
|
||||
#include "../utils/MovieEncoder.h"
|
||||
#include <memory>
|
||||
@@ -70,6 +72,7 @@ private:
|
||||
std::unique_ptr<UsdStageManager> m_stageManager;
|
||||
std::unique_ptr<LayerManager> m_layerManager;
|
||||
std::unique_ptr<PropertyManager> m_propertyManager;
|
||||
std::unique_ptr<MaterialManager> m_materialManager;
|
||||
CommandHistory m_commandHistory;
|
||||
std::unique_ptr<StageEditorPanel> m_stageEditorPanel;
|
||||
std::unique_ptr<SceneHierarchyPanel> m_sceneHierarchyPanel;
|
||||
@@ -77,6 +80,7 @@ private:
|
||||
std::unique_ptr<PropertyPanel> m_propertyPanel;
|
||||
std::unique_ptr<TimelinePanel> m_timelinePanel;
|
||||
std::unique_ptr<CurveEditorPanel> m_curveEditorPanel;
|
||||
std::unique_ptr<MaterialEditorPanel> m_materialEditorPanel;
|
||||
bool m_showDemoWindow;
|
||||
bool m_showStageInfo;
|
||||
bool m_showStageEditor;
|
||||
@@ -85,6 +89,7 @@ private:
|
||||
bool m_showPropertyPanel;
|
||||
bool m_showTimeline;
|
||||
bool m_showCurveEditor;
|
||||
bool m_showMaterialEditor;
|
||||
bool m_running;
|
||||
|
||||
MovieEncoder m_movieEncoder;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
|
||||
#include "../core/CommandHistory.h"
|
||||
#include "../core/MaterialManager.h"
|
||||
#include "IconManager.h"
|
||||
#include "MaterialPreviewRenderer.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <pxr/usd/sdf/valueTypeName.h>
|
||||
#include <pxr/base/vt/value.h>
|
||||
#include <imgui_node_editor.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// Node-graph editor for authoring UsdShade material networks, with a
|
||||
/// shader-ball preview rendered through Hydra.
|
||||
class MaterialEditorPanel {
|
||||
public:
|
||||
MaterialEditorPanel();
|
||||
~MaterialEditorPanel();
|
||||
|
||||
void SetStage(pxr::UsdStageRefPtr stage);
|
||||
void SetMaterialManager(MaterialManager* mgr) { m_materialManager = mgr; }
|
||||
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
|
||||
void SetIconManager(IconManager* icons) { m_iconManager = icons; }
|
||||
|
||||
/// Forwarded to the shader-ball preview so it matches the main viewport's
|
||||
/// global color-correction settings.
|
||||
void SetColorCorrectionFromPrefs(int ccMode, const std::string& ocioDisplay,
|
||||
const std::string& ocioView,
|
||||
const std::string& ocioColorSpace,
|
||||
const std::string& ocioLook);
|
||||
|
||||
/// Opens the material at pathStr in the graph work area, creating it (as a
|
||||
/// "Material" prim) if it doesn't exist yet. Also selects it in the browser.
|
||||
void OpenOrCreateMaterial(const std::string& pathStr);
|
||||
|
||||
/// Tracks the current selection (mirrors PropertyPanel/ViewportPanel's
|
||||
/// SetSelectedPrimPath) so the toolbar can offer "create + bind" / "bind"
|
||||
/// affordances, and so selecting a prim with an already-bound material
|
||||
/// auto-loads it into the canvas.
|
||||
void SetTargetPrimPath(const std::string& path);
|
||||
|
||||
void Render();
|
||||
|
||||
private:
|
||||
/// Resolved endpoint of a rendered pin, keyed by its ax::NodeEditor PinId.
|
||||
struct PinInfo {
|
||||
pxr::SdfPath nodePath;
|
||||
std::string name;
|
||||
bool isOutput;
|
||||
pxr::SdfValueTypeName typeName;
|
||||
};
|
||||
/// Resolved endpoint of a rendered link, keyed by its ax::NodeEditor LinkId.
|
||||
struct LinkInfo {
|
||||
pxr::SdfPath destNode;
|
||||
std::string destInput;
|
||||
};
|
||||
|
||||
void RenderToolbar();
|
||||
/// Property editor for the node selected in the graph, shown below the
|
||||
/// shader-ball preview. Values apply live while a widget is being
|
||||
/// dragged; one undoable command is pushed when the edit ends.
|
||||
void RenderSelectedNodeProperties();
|
||||
void RenderInputValueWidget(const ShaderGraphNode& node, const ShaderPinInfo& input,
|
||||
const pxr::UsdPrim& prim);
|
||||
void CommitInputEdit(const ShaderGraphNode& node, const ShaderPinInfo& input);
|
||||
/// Hypershade-style browser: lists every material on the stage; the
|
||||
/// selected one is loaded into the graph work area via "Show Graph"
|
||||
/// (or double-click).
|
||||
void RenderMaterialBrowser();
|
||||
/// Creates a uniquely-named empty material under /Materials and opens it.
|
||||
void CreateNewMaterial();
|
||||
void RenderNodeGraphCanvas();
|
||||
void RenderPreviewPanel();
|
||||
void HandleCreateAndDelete();
|
||||
/// Nuke-style TAB popup: type-to-filter shader node list; Enter or click
|
||||
/// creates the highlighted node at canvasPos.
|
||||
void RenderNodeSearchMenu(const ImVec2& canvasPos);
|
||||
void CreateShaderNode(const std::string& shaderId, const ImVec2& canvasPos);
|
||||
/// Binds an existing material to targetPath, undoably, restoring whatever
|
||||
/// direct binding (if any) targetPath had before.
|
||||
void BindMaterialToTarget(const pxr::SdfPath& materialPath, const pxr::SdfPath& targetPath);
|
||||
/// Creates a new material named after m_targetPrimPath under /Materials,
|
||||
/// binds it there, and opens it in the canvas.
|
||||
void CreateAndBindMaterialForTarget();
|
||||
void CreateConnection(const PinInfo& destInput, const PinInfo& sourceOutput);
|
||||
void DeleteNode(const pxr::SdfPath& nodePath);
|
||||
void DisconnectAttr(const pxr::SdfPath& destNode, const std::string& destInput);
|
||||
void SyncFromUsd();
|
||||
void PersistNodePosition(ax::NodeEditor::NodeId nodeId);
|
||||
bool IsPinLinked(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const;
|
||||
|
||||
static bool SaveNodeSettingsCallback(ax::NodeEditor::NodeId nodeId,
|
||||
const char* data, size_t size,
|
||||
ax::NodeEditor::SaveReasonFlags reason,
|
||||
void* userPointer);
|
||||
|
||||
ax::NodeEditor::EditorContext* m_editorContext = nullptr;
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
MaterialManager* m_materialManager = nullptr;
|
||||
CommandHistory* m_commandHistory = nullptr;
|
||||
IconManager* m_iconManager = nullptr;
|
||||
|
||||
/// Material currently shown in the graph work area (empty = none).
|
||||
pxr::SdfPath m_materialPath;
|
||||
/// Material highlighted in the browser list; becomes m_materialPath when
|
||||
/// the user clicks "Show Graph" (or double-clicks the entry).
|
||||
pxr::SdfPath m_browserSelection;
|
||||
/// Currently-selected prim, tracked for the "create + bind" / "bind"
|
||||
/// toolbar affordances; empty when nothing (or a non-prim path) is selected.
|
||||
pxr::SdfPath m_targetPrimPath;
|
||||
|
||||
ShaderGraphSnapshot m_graph;
|
||||
/// NodeId (uintptr_t) -> prim path, for nodes already known to the editor
|
||||
/// this session (seeded position, so re-syncing won't fight live drags).
|
||||
std::unordered_map<uintptr_t, pxr::SdfPath> m_nodeIdToPath;
|
||||
/// Rebuilt every frame from the current m_graph while rendering.
|
||||
std::unordered_map<uintptr_t, PinInfo> m_pinIdToInfo;
|
||||
std::unordered_map<uintptr_t, LinkInfo> m_linkIdToInfo;
|
||||
|
||||
MaterialPreviewRenderer m_preview;
|
||||
|
||||
ImVec2 m_pendingCreateNodePos{0.0f, 0.0f};
|
||||
|
||||
/// Alt+RMB drag distance not yet converted into a discrete wheel-zoom
|
||||
/// step (the node editor only zooms in wheel increments).
|
||||
float m_zoomDragAccum = 0.0f;
|
||||
|
||||
char m_nodeSearchBuf[128] = "";
|
||||
int m_nodeSearchSelected = 0;
|
||||
|
||||
/// 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
|
||||
/// canvas view center on the next RenderNodeGraphCanvas (ScreenToCanvas
|
||||
/// is only valid inside the editor's Begin/End).
|
||||
std::string m_pendingCreateShaderId;
|
||||
|
||||
/// First node currently selected in the graph editor (empty = none);
|
||||
/// drives the properties section under the preview.
|
||||
pxr::SdfPath m_selectedNodePath;
|
||||
/// Value/authored-state of the input being edited, captured when its
|
||||
/// widget activates, so the commit command can restore it on undo. Only
|
||||
/// one ImGui widget can be active at a time, so one slot suffices.
|
||||
pxr::VtValue m_preEditValue;
|
||||
bool m_preEditWasAuthored = false;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,332 @@
|
||||
#include "MaterialPreviewRenderer.h"
|
||||
#include "../utils/Logger.h"
|
||||
#include "../utils/PathUtils.h"
|
||||
#include <pxr/usd/usdGeom/sphere.h>
|
||||
#include <pxr/usd/usdGeom/metrics.h>
|
||||
#include <pxr/usd/usdGeom/tokens.h>
|
||||
#include <pxr/usd/usdShade/materialBindingAPI.h>
|
||||
#include <pxr/usd/usdShade/material.h>
|
||||
#include <pxr/usd/usdShade/shader.h>
|
||||
#include <pxr/usd/usdShade/connectableAPI.h>
|
||||
#include <pxr/usd/usdLux/distantLight.h>
|
||||
#include <pxr/usd/usdLux/domeLight.h>
|
||||
#include <pxr/usd/usdLux/lightAPI.h>
|
||||
#include <pxr/usd/usdLux/tokens.h>
|
||||
#include <pxr/usd/usdGeom/xformCommonAPI.h>
|
||||
#include <pxr/usd/usd/references.h>
|
||||
#include <pxr/usd/sdf/assetPath.h>
|
||||
#include <pxr/usd/sdf/primSpec.h>
|
||||
#include <imgui.h>
|
||||
#include <filesystem>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
namespace {
|
||||
const pxr::SdfPath kSpherePath("/Preview/Sphere");
|
||||
const pxr::SdfPath kDistantLightPath("/Preview/Light");
|
||||
const pxr::SdfPath kDomeLightPath("/Preview/DomeLight");
|
||||
|
||||
/// Scratch UsdPreviewSurface authored inside the (referenced) material scope
|
||||
/// when previewing a non-terminal node output — child of the material so the
|
||||
/// connection stays encapsulated.
|
||||
const pxr::TfToken kNodePreviewShaderName("__uslm_node_preview");
|
||||
|
||||
struct LightPreset {
|
||||
const char* label;
|
||||
const char* file; ///< under resources/hdri/ (CC0, Poly Haven; fetched by CMake)
|
||||
};
|
||||
const LightPreset kLightPresets[] = {
|
||||
{"External", "kloofendal_48d_partly_cloudy_puresky_1k.exr"},
|
||||
{"Room", "lebombo_1k.exr"},
|
||||
{"Interior", "artist_workshop_1k.exr"},
|
||||
{"Sunset", "venice_sunset_1k.exr"},
|
||||
};
|
||||
constexpr int kLightPresetCount = static_cast<int>(sizeof(kLightPresets) / sizeof(kLightPresets[0]));
|
||||
} // namespace
|
||||
|
||||
void MaterialPreviewRenderer::EnsureInitialized() {
|
||||
if (m_initialized) return;
|
||||
|
||||
m_previewStage = pxr::UsdStage::CreateInMemory();
|
||||
// Pin the up axis rather than inheriting the site fallback: the light
|
||||
// rig, dome-pole compensation, and camera framing all assume Y-up.
|
||||
pxr::UsdGeomSetStageUpAxis(m_previewStage, pxr::UsdGeomTokens->y);
|
||||
pxr::UsdGeomSphere sphere = pxr::UsdGeomSphere::Define(m_previewStage, kSpherePath);
|
||||
sphere.GetRadiusAttr().Set(1.0);
|
||||
|
||||
// Storm renders fine with zero authored lights (it injects a GL headlight
|
||||
// as a fallback), but proper Hydra delegates like Embree/Arnold shade
|
||||
// through scene lights only — without one they render solid black.
|
||||
// This distant light is the fallback when the HDR dome preset's texture
|
||||
// isn't bundled; ApplyLightPreset toggles between it and the dome.
|
||||
pxr::UsdLuxDistantLight light = pxr::UsdLuxDistantLight::Define(m_previewStage, kDistantLightPath);
|
||||
// pxr::UsdLuxLightAPI(light.GetPrim()).CreateIntensityAttr(pxr::VtValue(15000.0f));
|
||||
pxr::UsdLuxLightAPI(light.GetPrim()).CreateIntensityAttr(pxr::VtValue(1500.0f));
|
||||
// Default distant-light orientation shines along local -Z, which lights
|
||||
// the far side of the sphere from the camera's default view. Angle it
|
||||
// like a classic front-upper-left key light instead. Adding this scene
|
||||
// light suppresses UsdSceneRenderer's GL headlight fallback (it only
|
||||
// kicks in when the stage has no authored lights), so this needs to
|
||||
// carry the whole preview on its own.
|
||||
pxr::UsdGeomXformCommonAPI(light.GetPrim()).SetRotate(pxr::GfVec3f(-35.0f, -35.0f, 0.0f));
|
||||
|
||||
m_camera.SetStage(m_previewStage);
|
||||
m_renderer.SetStage(m_previewStage);
|
||||
m_renderer.SetShowGrid(false);
|
||||
// Raised refinement so the implicit sphere tessellates smoothly instead
|
||||
// of showing faceted silhouettes at swatch scale.
|
||||
m_renderer.SetComplexity(1.3f);
|
||||
|
||||
ApplyLightPreset(m_lightPreset);
|
||||
|
||||
m_previewBBox = pxr::GfBBox3d(m_renderer.ComputeStageBounds());
|
||||
m_camera.FrameSelection(m_previewBBox, 1.6); // extra margin so the sphere doesn't touch the frame edge
|
||||
m_renderer.SetCameraStateFromGfCamera(m_camera.ComputeGfCamera(m_previewBBox));
|
||||
|
||||
m_initialized = true;
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::SetMaterial(const pxr::UsdStageRefPtr& sourceStage,
|
||||
const pxr::SdfPath& materialPath,
|
||||
size_t graphRevision,
|
||||
const pxr::SdfPath& previewNodePath,
|
||||
const std::string& previewNodeOutput,
|
||||
bool previewOutputIsTerminal) {
|
||||
EnsureInitialized();
|
||||
|
||||
if (!sourceStage || materialPath.IsEmpty()) return;
|
||||
if (materialPath == m_materialPath && graphRevision == m_lastGraphRevision &&
|
||||
previewNodePath == m_previewNodePath && previewNodeOutput == m_previewNodeOutput)
|
||||
return; // nothing the shader ball cares about has changed
|
||||
|
||||
if (!sourceStage->GetPrimAtPath(materialPath)) {
|
||||
LOG_WARNING("MaterialPreviewRenderer: no prim at " + materialPath.GetString());
|
||||
return;
|
||||
}
|
||||
|
||||
// Compose the material into the preview stage via a reference to the
|
||||
// source stage's root layer, rather than SdfCopySpec from one local
|
||||
// layer: a material pulled in by a reference (e.g. a .mtlx asset) has no
|
||||
// spec in any local layer — it only exists composed — and even local
|
||||
// materials can be spread across sublayers. Referencing at the identical
|
||||
// path keeps internal connection paths valid without remapping, and
|
||||
// relative asset paths keep resolving against their original layers.
|
||||
pxr::UsdPrim mirrorPrim = m_previewStage->OverridePrim(materialPath);
|
||||
if (!mirrorPrim) {
|
||||
LOG_ERROR("MaterialPreviewRenderer: failed to create override at " + materialPath.GetString());
|
||||
return;
|
||||
}
|
||||
mirrorPrim.GetReferences().ClearReferences();
|
||||
mirrorPrim.GetReferences().AddReference(
|
||||
sourceStage->GetRootLayer()->GetIdentifier(), materialPath);
|
||||
|
||||
pxr::UsdPrim spherePrim = m_previewStage->GetPrimAtPath(kSpherePath);
|
||||
pxr::UsdShadeMaterial material(m_previewStage->GetPrimAtPath(materialPath));
|
||||
if (!material) {
|
||||
LOG_WARNING("MaterialPreviewRenderer: no material composed at " + materialPath.GetString());
|
||||
return;
|
||||
}
|
||||
if (spherePrim)
|
||||
pxr::UsdShadeMaterialBindingAPI::Apply(spherePrim).Bind(material);
|
||||
|
||||
// ── Selected-node preview (Hypershade-style) ─────────────────────────
|
||||
// Wipe any override left by a previous selection: the scratch wrapper
|
||||
// shader and the local opinions on the material's surface outputs, all
|
||||
// of which live only in the scratch stage's root layer.
|
||||
pxr::SdfLayerHandle rootLayer = m_previewStage->GetRootLayer();
|
||||
if (pxr::SdfPrimSpecHandle matSpec = rootLayer->GetPrimAtPath(materialPath)) {
|
||||
if (pxr::SdfPrimSpecHandle wrapSpec =
|
||||
rootLayer->GetPrimAtPath(materialPath.AppendChild(kNodePreviewShaderName)))
|
||||
matSpec->RemoveNameChild(wrapSpec);
|
||||
for (const char* outName : {"outputs:surface", "outputs:mtlx:surface"}) {
|
||||
if (pxr::SdfPropertySpecHandle prop =
|
||||
rootLayer->GetPropertyAtPath(materialPath.AppendProperty(pxr::TfToken(outName))))
|
||||
matSpec->RemoveProperty(prop);
|
||||
}
|
||||
}
|
||||
|
||||
// Route the material's surface through the selected node. Nodes outside
|
||||
// the material's subtree (possible in hand-authored cross-scope networks)
|
||||
// aren't composed into the scratch stage, so those keep the whole-material
|
||||
// preview.
|
||||
if (!previewNodePath.IsEmpty() && !previewNodeOutput.empty() &&
|
||||
previewNodePath.HasPrefix(materialPath)) {
|
||||
pxr::UsdShadeShader nodeShader(m_previewStage->GetPrimAtPath(previewNodePath));
|
||||
if (nodeShader) {
|
||||
pxr::UsdShadeConnectableAPI sourceApi = nodeShader.ConnectableAPI();
|
||||
pxr::TfToken sourceName(previewNodeOutput);
|
||||
if (!previewOutputIsTerminal) {
|
||||
// Pattern/texture output: show it as the diffuse color of a
|
||||
// scratch UsdPreviewSurface, like Maya's swatch for textures.
|
||||
pxr::UsdShadeShader wrap = pxr::UsdShadeShader::Define(
|
||||
m_previewStage, materialPath.AppendChild(kNodePreviewShaderName));
|
||||
wrap.CreateIdAttr(pxr::VtValue(pxr::TfToken("UsdPreviewSurface")));
|
||||
wrap.CreateInput(pxr::TfToken("roughness"), pxr::SdfValueTypeNames->Float)
|
||||
.Set(0.4f);
|
||||
wrap.CreateInput(pxr::TfToken("diffuseColor"), pxr::SdfValueTypeNames->Color3f)
|
||||
.ConnectToSource(sourceApi, sourceName);
|
||||
sourceApi = wrap.ConnectableAPI();
|
||||
sourceName = pxr::TfToken("surface");
|
||||
}
|
||||
// Override both the universal and the mtlx render-context outputs
|
||||
// so the preview target wins regardless of which context the
|
||||
// material authored (a .mtlx import only authors outputs:mtlx:*,
|
||||
// which Storm would otherwise prefer).
|
||||
material.CreateSurfaceOutput().ConnectToSource(sourceApi, sourceName);
|
||||
material.CreateSurfaceOutput(pxr::TfToken("mtlx")).ConnectToSource(sourceApi, sourceName);
|
||||
}
|
||||
}
|
||||
|
||||
m_materialPath = materialPath;
|
||||
m_lastGraphRevision = graphRevision;
|
||||
m_previewNodePath = previewNodePath;
|
||||
m_previewNodeOutput = previewNodeOutput;
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::ApplyLightPreset(int index) {
|
||||
if (index < 0 || index >= kLightPresetCount) index = 0;
|
||||
m_lightPreset = index;
|
||||
|
||||
const std::string file = ResourcePath(std::string("resources/hdri/") + kLightPresets[index].file);
|
||||
const bool haveHdr = std::filesystem::exists(file);
|
||||
|
||||
pxr::UsdPrim distant = m_previewStage->GetPrimAtPath(kDistantLightPath);
|
||||
if (haveHdr) {
|
||||
pxr::UsdLuxDomeLight dome = pxr::UsdLuxDomeLight::Define(m_previewStage, kDomeLightPath);
|
||||
dome.CreateTextureFileAttr().Set(pxr::SdfAssetPath(file));
|
||||
// Declare the projection explicitly: left at "automatic", hdArnold
|
||||
// falls through to Arnold's skydome default ("angular" fisheye),
|
||||
// which squeezes an equirect panorama into a blob at the center.
|
||||
// Storm always samples latlong regardless, so this only affects
|
||||
// spec-following delegates.
|
||||
dome.CreateTextureFormatAttr().Set(pxr::UsdLuxTokens->latlong);
|
||||
dome.GetPrim().SetActive(true);
|
||||
if (distant) distant.SetActive(false); // dome carries the lighting alone
|
||||
UpdateDomeOrientation();
|
||||
} else {
|
||||
LOG_WARNING("MaterialPreviewRenderer: HDRI not found (" + file +
|
||||
") — falling back to distant light. Re-run CMake configure to download presets.");
|
||||
if (pxr::UsdPrim dome = m_previewStage->GetPrimAtPath(kDomeLightPath))
|
||||
dome.SetActive(false);
|
||||
if (distant) distant.SetActive(true);
|
||||
}
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::UpdateDomeOrientation() {
|
||||
if (!m_previewStage) return;
|
||||
pxr::UsdPrim dome = m_previewStage->GetPrimAtPath(kDomeLightPath);
|
||||
if (!dome) return;
|
||||
|
||||
// Storm samples the dome texture with the pole along the light's local
|
||||
// +Y (domeLight.glslfx: v = acos(dir.y)/pi), while spec-following
|
||||
// delegates map it with the pole along local +Z (hdCycles feeds the
|
||||
// dome-local direction to Cycles' Z-up EnvironmentTextureNode; hdArnold's
|
||||
// skydome follows the same UsdLux convention). Rotating the dome -90°
|
||||
// about X for those points their +Z pole at world +Y, which reproduces
|
||||
// Storm's orientation exactly — including the horizontal (longitude)
|
||||
// alignment, since both parameterize u from the same axes.
|
||||
const pxr::TfToken rendererId = m_renderer.GetCurrentRendererId();
|
||||
const bool yUpPole = rendererId.IsEmpty() ||
|
||||
rendererId == pxr::TfToken("HdStormRendererPlugin");
|
||||
if (rendererId == pxr::TfToken("HdArnoldRendererPlugin")) {
|
||||
// With texture:format explicitly latlong, hdArnold aligns its skydome
|
||||
// with Storm on its own — earlier rotation offsets here were chasing
|
||||
// what turned out to be the angular-projection bug (format left
|
||||
// "automatic" fell through to Arnold's fisheye default). If a real
|
||||
// residual yaw shows up against Storm, put it in the Y component.
|
||||
pxr::UsdGeomXformCommonAPI(dome).SetRotate(pxr::GfVec3f(0.0f, 0.0f, 0.0f));
|
||||
} else {
|
||||
if(rendererId == pxr::TfToken("HdCyclesPlugin")) {
|
||||
// hdCycles' latlong mapping is flipped 180° from Storm's, so add
|
||||
// a yaw to match Storm's orientation. The pole is still along +Z.
|
||||
pxr::UsdGeomXformCommonAPI(dome).SetRotate(pxr::GfVec3f(yUpPole ? 0.0f : -90.0f, 270.0f, 0.0f));
|
||||
} else {
|
||||
pxr::UsdGeomXformCommonAPI(dome).SetRotate(pxr::GfVec3f(yUpPole ? 0.0f : -90.0f, 0.0f, 0.0f));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::RenderLightingDropdown() {
|
||||
if (!m_initialized) return;
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
if (ImGui::BeginCombo("##PreviewLighting", kLightPresets[m_lightPreset].label)) {
|
||||
for (int i = 0; i < kLightPresetCount; ++i) {
|
||||
if (ImGui::Selectable(kLightPresets[i].label, i == m_lightPreset) && i != m_lightPreset)
|
||||
ApplyLightPreset(i);
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Shader-ball HDR environment");
|
||||
}
|
||||
|
||||
uint32_t MaterialPreviewRenderer::Render(int width, int height) {
|
||||
EnsureInitialized();
|
||||
if (width <= 0 || height <= 0) return 0;
|
||||
|
||||
// Re-render while a progressive delegate (Arnold/Cycles/Embree) is still
|
||||
// accumulating — rendering only once would freeze its first noisy sample
|
||||
// pass into the cached texture. Storm converges in one pass, so the
|
||||
// dirty-flag caching still applies there.
|
||||
if (m_dirty || width != m_lastWidth || height != m_lastHeight || !m_renderer.IsConverged()) {
|
||||
// 2x supersample: the texture is displayed at (width, height) by
|
||||
// ImGui, so rendering larger gives the swatch cheap anti-aliasing.
|
||||
m_renderer.Render(width * 2, height * 2);
|
||||
m_dirty = false;
|
||||
m_lastWidth = width;
|
||||
m_lastHeight = height;
|
||||
}
|
||||
return m_renderer.GetColorTextureID();
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::OrbitDrag(float deltaX, float deltaY) {
|
||||
if (!m_initialized) return;
|
||||
m_camera.Tumble(deltaX * 0.5, deltaY * 0.5); // matches the viewport's tumble sensitivity
|
||||
m_renderer.SetCameraStateFromGfCamera(m_camera.ComputeGfCamera(m_previewBBox));
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::SetColorCorrection(int ccMode, const std::string& ocioDisplay,
|
||||
const std::string& ocioView,
|
||||
const std::string& ocioColorSpace,
|
||||
const std::string& ocioLook) {
|
||||
m_renderer.SetColorCorrectionMode(static_cast<ColorCorrectionMode>(ccMode));
|
||||
m_renderer.SetOcioDisplay(ocioDisplay);
|
||||
m_renderer.SetOcioView(ocioView);
|
||||
m_renderer.SetOcioColorSpace(ocioColorSpace);
|
||||
m_renderer.SetOcioLook(ocioLook);
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
void MaterialPreviewRenderer::RenderRendererDropdown() {
|
||||
pxr::TfToken currentId = m_renderer.GetCurrentRendererId();
|
||||
std::string displayName = currentId.IsEmpty()
|
||||
? "Renderer"
|
||||
: UsdSceneRenderer::GetRendererDisplayName(currentId);
|
||||
|
||||
ImGui::Button(displayName.c_str(), ImVec2(-1.0f, 0.0f));
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Shader-ball render delegate");
|
||||
|
||||
if (ImGui::BeginPopupContextItem("MaterialPreviewRendererPopup", ImGuiPopupFlags_MouseButtonLeft)) {
|
||||
for (const auto& pluginId : UsdSceneRenderer::GetRendererPlugins()) {
|
||||
std::string name = UsdSceneRenderer::GetRendererDisplayName(pluginId);
|
||||
if (name.empty()) name = pluginId.GetString();
|
||||
bool selected = (pluginId == currentId);
|
||||
if (ImGui::MenuItem(name.c_str(), nullptr, selected) && !selected) {
|
||||
m_renderer.SetRendererPlugin(pluginId);
|
||||
UpdateDomeOrientation(); // pole convention differs per delegate
|
||||
m_dirty = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include "../core/UsdSceneRenderer.h"
|
||||
#include "../core/ViewportCamera.h"
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <pxr/base/gf/bbox3d.h>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
|
||||
/// Shader-ball preview: mirrors the material currently open in the Material
|
||||
/// Editor onto a scratch sphere and renders it through an independent Hydra
|
||||
/// instance, with its own renderer-delegate switch (mirrors the viewport's
|
||||
/// dropdown so the swatch can preview Storm, Cycles, etc. independently of
|
||||
/// the main viewport's choice).
|
||||
class MaterialPreviewRenderer {
|
||||
public:
|
||||
/// Mirrors the material subtree at materialPath (composed via a reference
|
||||
/// to sourceStage's root layer) onto the internal scratch stage and binds
|
||||
/// it to the preview sphere. graphRevision is a cheap caller-computed
|
||||
/// hash of the current node/link graph + values — compared against the
|
||||
/// last call so unrelated frames don't pay for a re-render (real cost
|
||||
/// with a path-traced delegate).
|
||||
///
|
||||
/// previewNodePath (optional, Hypershade-style): the shader node selected
|
||||
/// in the graph; the ball then previews that node's output instead of the
|
||||
/// material's own surface. Terminal (token-typed) outputs are connected
|
||||
/// as the surface directly; pattern outputs are routed into the
|
||||
/// diffuseColor of a scratch UsdPreviewSurface. Pass an empty path to
|
||||
/// preview the whole material.
|
||||
void SetMaterial(const pxr::UsdStageRefPtr& sourceStage,
|
||||
const pxr::SdfPath& materialPath,
|
||||
size_t graphRevision,
|
||||
const pxr::SdfPath& previewNodePath = pxr::SdfPath(),
|
||||
const std::string& previewNodeOutput = std::string(),
|
||||
bool previewOutputIsTerminal = false);
|
||||
|
||||
/// Renders only if something changed since the last call. Returns the GL
|
||||
/// color texture ID suitable for ImGui::Image().
|
||||
uint32_t Render(int width, int height);
|
||||
|
||||
/// Click-drag orbit input, in pixel deltas (mirrors Maya's swatch orbit).
|
||||
void OrbitDrag(float deltaX, float deltaY);
|
||||
|
||||
void SetColorCorrection(int ccMode, const std::string& ocioDisplay,
|
||||
const std::string& ocioView,
|
||||
const std::string& ocioColorSpace,
|
||||
const std::string& ocioLook);
|
||||
|
||||
/// Renderer-delegate picker UI (matches the viewport's dropdown pattern).
|
||||
void RenderRendererDropdown();
|
||||
|
||||
/// HDR-environment picker (External / Room / Interior / Sunset). Presets
|
||||
/// map to bundled resources/hdri/*.exr dome-light textures; a preset
|
||||
/// whose file is missing falls back to the distant key light.
|
||||
void RenderLightingDropdown();
|
||||
|
||||
private:
|
||||
void EnsureInitialized();
|
||||
void ApplyLightPreset(int index);
|
||||
/// Compensates the dome-light pole convention per render delegate: Storm
|
||||
/// samples with the pole along local +Y, spec-following delegates
|
||||
/// (hdArnold, hdCycles) along local +Z. Called on preset and renderer
|
||||
/// switches so the HDR environment reads identically everywhere.
|
||||
void UpdateDomeOrientation();
|
||||
|
||||
pxr::UsdStageRefPtr m_previewStage;
|
||||
UsdSceneRenderer m_renderer;
|
||||
ViewportCamera m_camera;
|
||||
pxr::GfBBox3d m_previewBBox;
|
||||
|
||||
pxr::SdfPath m_materialPath; // path (identical in source & scratch stage)
|
||||
size_t m_lastGraphRevision = 0;
|
||||
pxr::SdfPath m_previewNodePath; // node whose output the ball previews (empty = whole material)
|
||||
std::string m_previewNodeOutput;
|
||||
int m_lightPreset = 0;
|
||||
bool m_initialized = false;
|
||||
bool m_dirty = true;
|
||||
int m_lastWidth = 0;
|
||||
int m_lastHeight = 0;
|
||||
};
|
||||
|
||||
} // namespace UsdLayerManager
|
||||
@@ -1240,8 +1240,13 @@ void PropertyPanel::RenderMaterialBindSection(const pxr::UsdPrim& prim) {
|
||||
ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Resolved");
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
if (resolved) {
|
||||
ImGui::TextColored(ImVec4(0.7f, 1.f, 0.7f, 1.f),
|
||||
"%s", resolved.GetPrim().GetPath().GetText());
|
||||
std::string matPathStr = resolved.GetPrim().GetPath().GetString();
|
||||
ImGui::TextColored(ImVec4(0.7f, 1.f, 0.7f, 1.f), "%s", matPathStr.c_str());
|
||||
if (OnEditMaterialRequested) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Edit Material"))
|
||||
OnEditMaterialRequested(matPathStr);
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("(none)");
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <pxr/base/gf/vec3d.h>
|
||||
#include <pxr/usd/usdGeom/xformCommonAPI.h>
|
||||
#include <imgui.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace UsdLayerManager {
|
||||
@@ -33,6 +34,11 @@ public:
|
||||
|
||||
void Render();
|
||||
|
||||
/// Fired when the user clicks "Edit Material" next to the resolved
|
||||
/// binding in the Material Binding section, with the resolved material's
|
||||
/// path. Wired by Application to open the Material Editor on that material.
|
||||
std::function<void(const std::string&)> OnEditMaterialRequested;
|
||||
|
||||
private:
|
||||
void ReadTransform();
|
||||
void WriteTranslate();
|
||||
|
||||
Reference in New Issue
Block a user