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:
+33
-1
@@ -13,6 +13,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(OpenUSD REQUIRED)
|
||||
find_package(Imgui REQUIRED)
|
||||
find_package(ImguiNodeEditor REQUIRED)
|
||||
find_package(Glad REQUIRED)
|
||||
find_package(FFmpeg REQUIRED)
|
||||
|
||||
@@ -195,6 +196,7 @@ target_include_directories(UsdLayerManager PRIVATE
|
||||
target_link_libraries(UsdLayerManager PRIVATE
|
||||
OpenUSD::OpenUSD
|
||||
Imgui::Imgui
|
||||
ImguiNodeEditor::ImguiNodeEditor
|
||||
Glad::Glad
|
||||
FFmpeg::FFmpeg
|
||||
OpenColorIO::OpenColorIO
|
||||
@@ -577,6 +579,33 @@ if(NOT EXISTS "${_ocio_config_dir}/config.ocio")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HDRI: shader-ball lighting presets (Poly Haven, CC0) at configure time
|
||||
# ---------------------------------------------------------------------------
|
||||
set(_hdri_dir "${CMAKE_SOURCE_DIR}/resources/hdri")
|
||||
set(_hdri_files
|
||||
kloofendal_48d_partly_cloudy_puresky_1k.exr
|
||||
lebombo_1k.exr
|
||||
artist_workshop_1k.exr
|
||||
venice_sunset_1k.exr
|
||||
)
|
||||
file(MAKE_DIRECTORY "${_hdri_dir}")
|
||||
foreach(_hdri ${_hdri_files})
|
||||
if(NOT EXISTS "${_hdri_dir}/${_hdri}")
|
||||
message(STATUS "Downloading HDRI ${_hdri} ...")
|
||||
file(DOWNLOAD
|
||||
"https://dl.polyhaven.org/file/ph-assets/HDRIs/exr/1k/${_hdri}"
|
||||
"${_hdri_dir}/${_hdri}"
|
||||
STATUS _hdri_dl_status
|
||||
)
|
||||
list(GET _hdri_dl_status 0 _hdri_dl_code)
|
||||
if(NOT _hdri_dl_code EQUAL 0)
|
||||
file(REMOVE "${_hdri_dir}/${_hdri}")
|
||||
message(WARNING "HDRI download failed for ${_hdri} (${_hdri_dl_status}) — the shader-ball lighting preset will fall back to a distant light.")
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Copy resources to build directory
|
||||
file(COPY ${CMAKE_SOURCE_DIR}/resources
|
||||
DESTINATION ${CMAKE_BINARY_DIR}
|
||||
@@ -592,7 +621,10 @@ add_custom_command(TARGET UsdLayerManager POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_SOURCE_DIR}/resources/icons"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/icons"
|
||||
COMMENT "Copying fonts and SVG icons to build output"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_SOURCE_DIR}/resources/hdri"
|
||||
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/hdri"
|
||||
COMMENT "Copying fonts, SVG icons and HDRI presets to build output"
|
||||
)
|
||||
|
||||
# Copy ACES OCIO config to build output — only on first build (skipped if already present).
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# FindImguiNodeEditor.cmake - Find or configure thedmd/imgui-node-editor
|
||||
#
|
||||
# This module looks for imgui-node-editor in the third_party/imgui-node-editor
|
||||
# directory (vendored, docking branch) and compiles its sources directly
|
||||
# against this project's own Imgui::Imgui target so it shares the exact same
|
||||
# ImGui headers/defines (no second copy of ImGui is pulled in).
|
||||
#
|
||||
# Inputs:
|
||||
# IMGUI_NODE_EDITOR_DIR - Path to imgui-node-editor/NodeEditor root
|
||||
#
|
||||
# Outputs:
|
||||
# ImguiNodeEditor_FOUND
|
||||
# ImguiNodeEditor::ImguiNodeEditor (imported STATIC target)
|
||||
|
||||
if(NOT IMGUI_NODE_EDITOR_DIR)
|
||||
set(IMGUI_NODE_EDITOR_DIR "${CMAKE_SOURCE_DIR}/third_party/imgui-node-editor/NodeEditor")
|
||||
endif()
|
||||
|
||||
if(NOT IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR)
|
||||
set(IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR
|
||||
"${CMAKE_SOURCE_DIR}/third_party/imgui-node-editor/Examples/Common/BlueprintUtilities")
|
||||
endif()
|
||||
|
||||
find_path(ImguiNodeEditor_INCLUDE_DIR
|
||||
NAMES imgui_node_editor.h
|
||||
PATHS "${IMGUI_NODE_EDITOR_DIR}/Include"
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
|
||||
set(ImguiNodeEditor_SOURCES
|
||||
"${IMGUI_NODE_EDITOR_DIR}/Source/crude_json.cpp"
|
||||
"${IMGUI_NODE_EDITOR_DIR}/Source/imgui_canvas.cpp"
|
||||
"${IMGUI_NODE_EDITOR_DIR}/Source/imgui_node_editor_api.cpp"
|
||||
"${IMGUI_NODE_EDITOR_DIR}/Source/imgui_node_editor.cpp"
|
||||
# Blueprint pin-icon widgets (Widgets/Drawing only — Builders.cpp is
|
||||
# intentionally excluded, it needs Spring()/BeginHorizontal()/BeginVertical()
|
||||
# from thedmd's own ImGui fork, which this project's vanilla ImGui lacks).
|
||||
"${IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR}/Source/ax/Drawing.cpp"
|
||||
"${IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR}/Source/ax/Widgets.cpp"
|
||||
)
|
||||
|
||||
foreach(_src ${ImguiNodeEditor_SOURCES})
|
||||
if(NOT EXISTS "${_src}")
|
||||
set(ImguiNodeEditor_SOURCES_MISSING TRUE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(ImguiNodeEditor
|
||||
REQUIRED_VARS ImguiNodeEditor_INCLUDE_DIR
|
||||
FAIL_MESSAGE "imgui-node-editor not found — expected it vendored at third_party/imgui-node-editor/NodeEditor"
|
||||
)
|
||||
|
||||
if(ImguiNodeEditor_FOUND AND ImguiNodeEditor_SOURCES_MISSING)
|
||||
message(FATAL_ERROR "imgui-node-editor sources missing under ${IMGUI_NODE_EDITOR_DIR}/Source")
|
||||
endif()
|
||||
|
||||
if(ImguiNodeEditor_FOUND AND NOT TARGET ImguiNodeEditor::ImguiNodeEditor)
|
||||
add_library(imgui_node_editor_impl STATIC ${ImguiNodeEditor_SOURCES})
|
||||
|
||||
target_include_directories(imgui_node_editor_impl
|
||||
PUBLIC "${ImguiNodeEditor_INCLUDE_DIR}"
|
||||
"${IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR}/Include"
|
||||
PRIVATE "${IMGUI_NODE_EDITOR_DIR}/Source"
|
||||
"${IMGUI_NODE_EDITOR_BLUEPRINT_UTILITIES_DIR}/Source"
|
||||
)
|
||||
|
||||
# Must be defined before the first #include <imgui.h> in every translation
|
||||
# unit (imgui_internal.h hard-errors otherwise) — a command-line define
|
||||
# guarantees that ordering regardless of vendored include order.
|
||||
target_compile_definitions(imgui_node_editor_impl PRIVATE IMGUI_DEFINE_MATH_OPERATORS)
|
||||
|
||||
# Must share this project's own ImGui build (same headers/defines) rather
|
||||
# than pulling in a second ImGui — imgui-node-editor only needs the public
|
||||
# API plus ImRect/ImFloor from imgui_internal.h.
|
||||
target_link_libraries(imgui_node_editor_impl PUBLIC Imgui::Imgui)
|
||||
|
||||
add_library(ImguiNodeEditor::ImguiNodeEditor ALIAS imgui_node_editor_impl)
|
||||
message(STATUS "imgui-node-editor found: ${ImguiNodeEditor_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
mark_as_advanced(ImguiNodeEditor_INCLUDE_DIR)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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