Compare commits

..

13 Commits

Author SHA1 Message Date
indigo 05201465be Validate persisted OCIO preferences against the active config
A display or view saved in preferences.ini that doesn't exist in the current
OCIO config made HdxColorCorrectionTask throw every frame
("DisplayViewTransform error. Display 'x' not found") and silently skip color
correction, which reads as a washed-out viewport -- worst on delegates like
hdEmbree whose output depends on the transform actually running.

The two are easy to invert: in the bundled ACES 1.2 config the only display is
"ACES" and "sRGB" is one of its views, but the shipped preferences had
OcioDisplay=sRGB and OcioView="ACES 1.0 SDR-video" (an OCIO-v2 studio-config
view that doesn't exist here). Nothing validated them -- LoadPreferences fed
ApplyGlobalColorCorrection directly, and the only existing fallback lived in
the preferences dialog and triggered on empty, never on invalid.

ValidateOcioPreferences() now runs right after LoadPreferences() so both the
viewport and the Material Editor see corrected values:
- display falls back to the config default when missing
- view is checked against the *resolved* display's view list, since correcting
  the display can invalidate the view; prefers the config default view when
  valid for that display, else its first view
- color space and look are cleared when they name something absent, as they
  fail the same way (note the real name is "ACES - ACEScg", not "ACEScg")
- no-op when the config is unreadable, so a missing $OCIO doesn't clobber
  values that may be correct for a config supplied later

Each correction logs a warning naming the old and new value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:24:24 +08:00
indigo a907ec0182 Add collapsible Sublayers section to Scene Hierarchy panel
Puts the sublayer stack directly under the edit-target combo in the Scene
Hierarchy, so muting and reordering no longer require switching to the
separate Stage Editor window.

Each row mirrors StageEditorPanel: drag handle with drag-drop reorder, dirty
indicator, name (dimmed when muted, [RL] badge for render-layer sublayers),
edit-target checkbox and mute checkbox. Adds dedicated up/down chevron icon
buttons for reordering, which the Stage Editor only exposed as context-menu
items. Reorder goes through the existing LayerReorderCommand so it is undoable;
mute calls LayerManager directly, matching current app-wide behaviour.

New Icon::ChevronUp / ChevronDown with matching SVGs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 10:03:05 +08:00
indigo b5c1e50438 Add Maya-style Render Layer panel
Render layers built on the existing USD sublayer architecture: each layer is a
file-backed SdfLayer mounted as a sublayer and tagged via custom layer data,
with membership stored in a UsdCollectionAPI on a hidden /RenderLayerData prim.
Activating a layer applies its overrides and isolates it to its members.
"Default" is virtual -- it means all render-layer sublayers muted.

See docs/adr/0002-render-layer.md for the design and rationale.

- RenderLayerManager: create/delete/rename/activate, membership, mute-based
  isolation. Sdf-level APIs are used while a layer is muted (Usd-level APIs
  can't see muted layers) and Usd-level APIs when active.
- Commands: create/delete/rename/activate and add/remove members, all undoable.
- RenderLayerPanel: layer list with one-click activation and a membership
  editor for the selected prims.
- StageEditorPanel / SceneHierarchyPanel: badge render-layer-owned sublayers,
  disable their mute/remove controls (they're managed from the Render Layer
  panel so the two can't desync), and lock the edit target while a non-Default
  layer is active.
- ConnectShaderAttrs/DisconnectShaderAttr: take an explicit editLayer captured
  at construction, so undo targets the same layer as execute even if the
  ambient edit target changed in between (e.g. a render-layer switch).

Note: SceneHierarchyPanel.h also carries declarations for the sublayers
section added in the following commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 10:02:33 +08:00
indigo e2a4961ebe Upgrade to OpenUSD 25.11 (Python 3.12) and fix Arnold/Embree regressions
Build against OpenUSD 25.11 built from source with Python 3.12, OCIO 2.2.1,
OpenVDB, Ptex and Embree, replacing the 25.05/py311 mix.

Build system:
- CMAKE_PREFIX_PATH -> third_party/OpenUSD-v25.11; HDARNOLD_ROOT ->
  hdArnold-v25.11. hdEmbree is bundled in the USD build now, so
  HDEMBREE_USD_ROOT is no longer needed.
- FindOpenUSD: drop usd_ndr (ndr was merged into sdr in 25.11).
- hdArnold plugin renames: ndrArnold -> nodeRegistryArnold, plus the new
  usdImagingArnold plugin.
- Glob the version-suffixed OpenColorIO_*.dll instead of hardcoding 2_1.
- Drop the python311.dll workaround; USD and Cycles now share Python 3.12.
- Cycles: deploy OpenColorIO_2_5.dll (needed by its bundled OpenImageIO) and
  stop the debug-DLL filter from eating IlmThread.dll -- the regex `d[.]dll$`
  also matched legitimate Release DLLs.

Runtime fixes:
- cullStyle now defaults to Nothing, matching usdview (viewSettingsDataModel
  cullBackfaces=False). BackUnlessDoubleSided drops back faces on geometry
  that isn't authored doubleSided, which hdEmbree applies to occlusion rays
  too, so interiors shaded as single-sided.
- Set HDARNOLD_osl_includepath (and PXR_MTLX_STDLIB_SEARCH_PATHS) in main.cpp
  before the plugin DLL pre-load. hdArnold compiles MaterialX via generated
  OSL that begins with #include "mx_funcs.h"; without an include path Arnold
  fails with "fatal error: 'mx_funcs.h' file not found". It must be set before
  any plugin loads because TF_DEFINE_ENV_SETTING caches the value when
  hdArnold.dll registers its settings.
- Deploy the MaterialX standard library next to the executable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 10:02:00 +08:00
indigo 9d61819636 Update README to reflect Material Editor, Timeline/Curve Editor, and OCIO color management
README hadn't been touched since the early layer/viewport-only stage of
the project: it still described the removed LayerPanel, omitted the
Material Editor / MaterialPreviewRenderer / NodeThumbnailCache /
CurveEditorPanel / TimelinePanel entirely, and its Tests section
claimed ctest works even though BUILD_TESTS is OFF in every preset
(the two test executables have stale USD include paths, per
CLAUDE.md). Refreshes the feature list, architecture tree, and build
docs to match current source, and adds a screenshot gallery for the
Material Editor, Curve Editor, quad viewport, and MaterialX import.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 17:45:04 +08:00
indigo 27c619c518 Add screenshots verifying Arnold node thumbnail convergence fix
Material Editor with Arnold-shaded RustMetalMat: node thumbnails
(texture and standard_surface shader ball) now render converged
under the Arnold delegate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 17:39:56 +08:00
indigo 196a2bb0dd Fix node thumbnails not converging with progressive renderers; add colorSpace and Sdr-enum widgets
- NodeThumbnailCache/MaterialPreviewRenderer: keep a material node
  thumbnail stale (re-pumped each frame) until the shared preview
  renderer actually converges, instead of caching after one render
  pass. Progressive delegates (Arnold/Cycles/Embree) need multiple
  passes to accumulate past their first noisy sample, so thumbnails
  previously froze on an early frame and only "fixed themselves"
  when toggling thumbnail display forced a fresh render.
- MaterialEditorPanel: add a Color Space row under Asset-typed
  shader inputs, authoring the attribute's colorSpace metadata from
  the active OCIO config's color spaces.
- MaterialManager: surface Sdr-declared allowed values (enums) per
  shader input so the property panel can render a dropdown instead
  of free text.
- CMakePresets.json: build the release preset with debug info
  (RelWithDebInfo) for easier diagnosis.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 17:39:14 +08:00
indigo 6cb1b45496 Delete unuse icon 2026-07-11 11:08:59 +08:00
indigo a877e8973c Add thumbnail toggle and set-all pin-display toolbar icons
Material editor toolbar gets an eye icon to hide/show node thumbnails
(skips the reserved space and decode/render work, not just the
visual) and a 3-dot icon that broadcasts a pin-display mode (All /
Connected / Collapsed) to every node in the open material at once.
The dot icon is shared with the per-node toggle via a parameterized
DrawPinDisplayModeIcon, with dot radius/spacing scaled to fit the
toolbar's fixed icon-button size instead of overflowing it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 11:07:57 +08:00
indigo a7314ff108 Re-author eye, eye-slash, file-plus, and swatchbook icons in Inkscape
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 11:07:46 +08:00
indigo bff42082a5 Add Collapsed pin display mode with a 3-dot state icon
Cycle each node through All -> Connected -> Collapsed -> All via a
custom-drawn icon (filled-dot count encodes how much is shown: 3/2/1).
Collapsed hides every pin on a side, linked or not, behind one
connector nub; existing links now redirect to that nub too
(EffectivePinId), so they visually converge like Hypershade's collapse
view instead of only hiding unconnected pins. Also moved the connector
nubs off the title row onto the node's bottom row. Persisted mode
switches from a bool (uiShowAllPins) to an int (uiPinDisplayMode).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 01:52:52 +08:00
indigo c7edd145df Hypershade-style pin display modes for material graph nodes
Each node gets an All/Conn toggle: connected-only mode hides
unconnected pins behind collapsed connector nubs on the title row;
dragging a link onto (or from) a nub opens a menu to pick which hidden
pin to wire, and the pin appears once connected. The toggle is drawn
and hit-tested geometrically, and its tooltip is deferred to after
NE::End() so it lands at the cursor. A new Preferences > Material
checkbox opts into persisting the per-node mode as USD customData
(uiShowAllPins); otherwise it stays session-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:02:56 +08:00
indigo 22406fbe63 Fix ImGui hover dead zones at negative canvas coordinates
Port upstream imgui-node-editor's IMGUI_HAS_VIEWPORT support into the
vendored canvas: docking-branch ImGui's IsMouseHoveringRect requires
rects to overlap the mouse viewport's screen-space rect, so items at
canvas coordinates outside the screen range (e.g. negative) never
hovered or clicked. Transform the viewport into canvas-local space in
EnterLocalSpace() and restore it in LeaveLocalSpace(), like the mouse
position. Fixes node hover highlight and in-node widget interaction
left of / above the canvas origin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:02:44 +08:00
48 changed files with 3234 additions and 154 deletions
+1
View File
@@ -1,5 +1,6 @@
# Build directories # Build directories
build/ build/
build_*/
install/ install/
out/ out/
.kilocode/ .kilocode/
+3 -3
View File
@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Build ## Build
**Prerequisites:** MSVC 2022, CMake ≥ 3.20, Python 3.12 at `%LOCALAPPDATA%\Programs\Python\Python312`. **Prerequisites:** MSVC 2022, CMake ≥ 3.20, Python 3.12 at `%LOCALAPPDATA%\Programs\Python\Python312`.
OpenUSD 25.05 is expected at `third_party/OpenUSD-v25.05` (set via `CMAKE_PREFIX_PATH` in the preset). OpenUSD 25.11 is expected at `third_party/OpenUSD-v25.11` (set via `CMAKE_PREFIX_PATH` in the preset).
```powershell ```powershell
# Configure (one-time; downloads ACES 1.2 OCIO config ~124 MB on first run) # Configure (one-time; downloads ACES 1.2 OCIO config ~124 MB on first run)
@@ -20,11 +20,11 @@ cmake --build build --config Release
cmake --build build --config Release --target install cmake --build build --config Release --target install
``` ```
The `default` preset (in `CMakePresets.json`) enables `WITH_CYCLES=ON`, `HDARNOLD_ROOT`, and `HDEMBREE_USD_ROOT` for the maintainer's machine. For a minimal build without optional render delegates, configure manually: The `default` preset (in `CMakePresets.json`) enables `WITH_CYCLES=ON` and `HDARNOLD_ROOT` for the maintainer's machine. hdEmbree is bundled directly in the `third_party/OpenUSD-v25.11` build (built with `--embree`), so no separate `HDEMBREE_USD_ROOT` is needed. For a minimal build without optional render delegates, configure manually:
```powershell ```powershell
cmake -B build -G "Visual Studio 17 2022" ` cmake -B build -G "Visual Studio 17 2022" `
-DCMAKE_PREFIX_PATH="third_party/OpenUSD-v25.05" ` -DCMAKE_PREFIX_PATH="third_party/OpenUSD-v25.11" `
-DIMGUI_DIR="third_party/imgui-1.92.7" ` -DIMGUI_DIR="third_party/imgui-1.92.7" `
-DWITH_CYCLES=OFF -DWITH_CYCLES=OFF
``` ```
+70 -40
View File
@@ -63,9 +63,13 @@ set(ARNOLD_LOCATION "" CACHE PATH "Arnold SDK root (e.g. C:/Autodesk/mtoa/5.5.0/
if(HDARNOLD_ROOT AND EXISTS "${HDARNOLD_ROOT}/plugin/hdArnold.dll") if(HDARNOLD_ROOT AND EXISTS "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
set(OPENUSD_HAS_ARNOLD TRUE) set(OPENUSD_HAS_ARNOLD TRUE)
set(HDARNOLD_DLL "${HDARNOLD_ROOT}/plugin/hdArnold.dll") set(HDARNOLD_DLL "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
set(NDRARNOLD_DLL "${HDARNOLD_ROOT}/plugin/ndrArnold.dll")
set(HDARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/hdArnold") set(HDARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/hdArnold")
set(NDRARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/ndrArnold") # arnold-usd renamed the ndr plugin to node_registry (USD merged ndr into
# sdr as of PXR_VERSION 2505) and added a usdImaging adapter plugin.
set(NDRARNOLD_DLL "${HDARNOLD_ROOT}/plugin/nodeRegistryArnold.dll")
set(NDRARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/nodeRegistryArnold")
set(USDIMAGINGARNOLD_DLL "${HDARNOLD_ROOT}/plugin/usdImagingArnold.dll")
set(USDIMAGINGARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/usdImagingArnold")
if(ARNOLD_LOCATION AND EXISTS "${ARNOLD_LOCATION}/bin/ai.dll") if(ARNOLD_LOCATION AND EXISTS "${ARNOLD_LOCATION}/bin/ai.dll")
set(ARNOLD_AI_DLL "${ARNOLD_LOCATION}/bin/ai.dll") set(ARNOLD_AI_DLL "${ARNOLD_LOCATION}/bin/ai.dll")
# ai.dll also requires companion runtime DLLs (OIDN denoiser, Adsk licensing, cer) # ai.dll also requires companion runtime DLLs (OIDN denoiser, Adsk licensing, cer)
@@ -98,6 +102,12 @@ set(HDCYCLES_PLUGIN_DIR "${CYCLES_INSTALL_DIR}/hydra/hdCycles")
# tbb12.dll is required by openvdb/embree4 but lives in the precompiled lib tree, # tbb12.dll is required by openvdb/embree4 but lives in the precompiled lib tree,
# not in the install output — deploy it explicitly. # not in the install output — deploy it explicitly.
set(CYCLES_TBB12_DLL "${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/tbb/bin/tbb12.dll") set(CYCLES_TBB12_DLL "${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/tbb/bin/tbb12.dll")
# OpenColorIO_2_5.dll: Cycles' bundled prebuilt OpenImageIO.dll was compiled
# against Cycles' own OCIO 2.5 (forced via -DOpenColorIO_DIR below, to avoid
# USD's OCIO headers shadowing Cycles' during the ExternalProject build) —
# distinct from and required alongside USD's own OpenColorIO_2_2.dll. It also
# lives only in the precompiled lib tree, not in cycles_install's output.
set(CYCLES_OCIO25_DLL "${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/opencolorio/bin/OpenColorIO_2_5.dll")
if(WITH_CYCLES) if(WITH_CYCLES)
include(ExternalProject) include(ExternalProject)
@@ -228,13 +238,20 @@ if(WIN32)
_CRT_SECURE_NO_WARNINGS _CRT_SECURE_NO_WARNINGS
) )
# OpenColorIO DLL — copy from USD bin/ (which already ships OpenColorIO_2_1.dll) # OpenColorIO DLL — copy from USD bin/. Filename is version-suffixed
# (e.g. OpenColorIO_2_2.dll) and changes whenever the USD build's bundled
# OCIO version changes, so glob for it instead of hardcoding the version.
file(GLOB OPENCOLORIO_RUNTIME_DLL "${OpenUSD_ROOT_DIR}/bin/OpenColorIO_*.dll")
if(OPENCOLORIO_RUNTIME_DLL)
add_custom_command(TARGET UsdLayerManager POST_BUILD add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${OpenUSD_ROOT_DIR}/bin/OpenColorIO_2_1.dll" ${OPENCOLORIO_RUNTIME_DLL}
"$<TARGET_FILE_DIR:UsdLayerManager>" "$<TARGET_FILE_DIR:UsdLayerManager>"
COMMENT "Copying OpenColorIO runtime DLL..." COMMENT "Copying OpenColorIO runtime DLL..."
) )
else()
message(WARNING "OpenColorIO runtime DLL not found in ${OpenUSD_ROOT_DIR}/bin")
endif()
# FFmpeg DLLs — copy all .dll files from third_party/ffmpeg/bin # FFmpeg DLLs — copy all .dll files from third_party/ffmpeg/bin
file(GLOB FFMPEG_RUNTIME_DLLS "${CMAKE_SOURCE_DIR}/third_party/ffmpeg/bin/*.dll") file(GLOB FFMPEG_RUNTIME_DLLS "${CMAKE_SOURCE_DIR}/third_party/ffmpeg/bin/*.dll")
@@ -323,13 +340,19 @@ if(WIN32)
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold.dll" "$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold.dll"
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${NDRARNOLD_DLL}" "${NDRARNOLD_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/ndrArnold.dll" "$<TARGET_FILE_DIR:UsdLayerManager>/usd/nodeRegistryArnold.dll"
COMMAND ${CMAKE_COMMAND} -E copy_directory COMMAND ${CMAKE_COMMAND} -E copy_directory
"${HDARNOLD_PLUGIN_DIR}" "${HDARNOLD_PLUGIN_DIR}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold" "$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold"
COMMAND ${CMAKE_COMMAND} -E copy_directory COMMAND ${CMAKE_COMMAND} -E copy_directory
"${NDRARNOLD_PLUGIN_DIR}" "${NDRARNOLD_PLUGIN_DIR}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/ndrArnold" "$<TARGET_FILE_DIR:UsdLayerManager>/usd/nodeRegistryArnold"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${USDIMAGINGARNOLD_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/usdImagingArnold.dll"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${USDIMAGINGARNOLD_PLUGIN_DIR}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/usdImagingArnold"
COMMENT "Copying hdArnold plugin..." COMMENT "Copying hdArnold plugin..."
) )
if(ARNOLD_RUNTIME_DLLS) if(ARNOLD_RUNTIME_DLLS)
@@ -367,8 +390,10 @@ if(WIN32)
" endif()\n" " endif()\n"
# Exclude debug-variant DLLs (not needed in release, just wasted space). # Exclude debug-variant DLLs (not needed in release, just wasted space).
# Pattern: OpenColorIO_d_2_5.dll → contains "_d_" # Pattern: OpenColorIO_d_2_5.dll → contains "_d_"
# openjph.0.25d.dll → ends with "d.dll" # openjph.0.25d.dll → version digit directly followed by "d.dll"
" if(_lname MATCHES \"_d_\" OR _lname MATCHES \"d[.]dll$\")\n" # NOTE: must require a digit before the trailing "d" — a bare "d[.]dll$"
# also matches legitimate Release DLLs like IlmThread.dll (ends in "...ead.dll").
" if(_lname MATCHES \"_d_\" OR _lname MATCHES \"[0-9]d[.]dll$\")\n"
" continue()\n" " continue()\n"
" endif()\n" " endif()\n"
" list(APPEND _dlls \"\${_dll}\")\n" " list(APPEND _dlls \"\${_dll}\")\n"
@@ -391,6 +416,10 @@ if(WIN32)
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CYCLES_TBB12_DLL}" "${CYCLES_TBB12_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/tbb12.dll" "$<TARGET_FILE_DIR:UsdLayerManager>/tbb12.dll"
# OpenColorIO_2_5.dll is required by Cycles' bundled OpenImageIO.dll
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CYCLES_OCIO25_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/OpenColorIO_2_5.dll"
COMMENT "Copying hdCycles plugin and runtime DLLs..." COMMENT "Copying hdCycles plugin and runtime DLLs..."
) )
endif() endif()
@@ -468,6 +497,14 @@ if(OPENUSD_HAS_ARNOLD)
DESTINATION bin/usd DESTINATION bin/usd
OPTIONAL OPTIONAL
) )
install(FILES "${USDIMAGINGARNOLD_DLL}"
DESTINATION bin/usd
OPTIONAL
)
install(DIRECTORY "${USDIMAGINGARNOLD_PLUGIN_DIR}"
DESTINATION bin/usd
OPTIONAL
)
if(ARNOLD_RUNTIME_DLLS) if(ARNOLD_RUNTIME_DLLS)
install(FILES ${ARNOLD_RUNTIME_DLLS} install(FILES ${ARNOLD_RUNTIME_DLLS}
DESTINATION bin DESTINATION bin
@@ -511,32 +548,20 @@ if(OPENUSD_HAS_CYCLES)
DESTINATION bin DESTINATION bin
OPTIONAL OPTIONAL
) )
# OpenColorIO_2_5.dll (used by Cycles' bundled OpenImageIO.dll) — same story
install(FILES "${CYCLES_OCIO25_DLL}"
DESTINATION bin
OPTIONAL
)
endif() endif()
# Install Python runtime DLLs required by OpenUSD and Cycles. # Install Python runtime DLLs required by OpenUSD and Cycles.
# python312.dll : linked by Cycles (hdCycles.dll → python312.dll) # python312.dll: linked by both Cycles (hdCycles.dll) and USD's Python bindings
# python311.dll : linked by usd_python.dll (the OpenUSD Python bindings were # (usd_python.dll) — the OpenUSD build and Cycles now share the same Python 3.12.
# built against Python 3.11, regardless of what we compile with)
# Detect python311.dll from common install paths.
foreach(_pyroot
"$ENV{LOCALAPPDATA}/Programs/Python/Python311"
"C:/Python311"
"C:/Program Files/Python311"
)
if(EXISTS "${_pyroot}/python311.dll")
set(PYTHON311_DLL "${_pyroot}/python311.dll")
message(STATUS "Found python311.dll (required by usd_python.dll): ${PYTHON311_DLL}")
break()
endif()
endforeach()
set(_python_runtime_dlls set(_python_runtime_dlls
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll" "$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll"
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll" "$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll"
) )
if(PYTHON311_DLL)
list(APPEND _python_runtime_dlls "${PYTHON311_DLL}")
endif()
install(FILES ${_python_runtime_dlls} install(FILES ${_python_runtime_dlls}
DESTINATION bin DESTINATION bin
@@ -544,17 +569,6 @@ install(FILES ${_python_runtime_dlls}
# system-wide Python; in that case the DLL is on PATH already. # system-wide Python; in that case the DLL is on PATH already.
) )
# Also copy python311.dll to the build output directory so debug runs work
# without requiring C:\Python311 to be on PATH.
if(PYTHON311_DLL)
add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${PYTHON311_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/python311.dll"
COMMENT "Copying python311.dll (for usd_python.dll)..."
)
endif()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# OCIO: download ACES 1.2 config at configure time if not present # OCIO: download ACES 1.2 config at configure time if not present
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -662,7 +676,15 @@ add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/resources/vdb" "${CMAKE_SOURCE_DIR}/resources/vdb"
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/vdb" "$<TARGET_FILE_DIR:UsdLayerManager>/resources/vdb"
COMMENT "Copying fonts, SVG icons, HDRI presets and preview shapes to build output" # MaterialX standard library (nodedefs + genosl headers such as mx_funcs.h).
# hdArnold reads PXR_MTLX_STDLIB_SEARCH_PATHS to locate these; without them
# Arnold's OSL compile of MaterialX shaders fails with
# "fatal error: 'mx_funcs.h' file not found". Application.cpp points the
# env var at this deployed copy.
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${OpenUSD_ROOT_DIR}/libraries"
"$<TARGET_FILE_DIR:UsdLayerManager>/libraries"
COMMENT "Copying fonts, SVG icons, HDRI presets, preview shapes and MaterialX stdlib to build output"
) )
# Copy ACES OCIO config to build output — only on first build (skipped if already present). # Copy ACES OCIO config to build output — only on first build (skipped if already present).
@@ -678,7 +700,7 @@ endif()
# Install OpenColorIO DLL (from the OpenUSD distribution's bin/) # Install OpenColorIO DLL (from the OpenUSD distribution's bin/)
install(FILES install(FILES
"${OpenUSD_ROOT_DIR}/bin/OpenColorIO_2_1.dll" ${OPENCOLORIO_RUNTIME_DLL}
DESTINATION bin DESTINATION bin
OPTIONAL OPTIONAL
) )
@@ -689,6 +711,14 @@ install(DIRECTORY ${CMAKE_SOURCE_DIR}/resources
DESTINATION bin DESTINATION bin
) )
# MaterialX standard library — required by hdArnold's OSL codegen (mx_funcs.h).
# See the matching POST_BUILD copy above and PXR_MTLX_STDLIB_SEARCH_PATHS in
# Application.cpp.
install(DIRECTORY "${OpenUSD_ROOT_DIR}/libraries"
DESTINATION bin
OPTIONAL
)
# Install Inter font next to the exe under resources/font/ # Install Inter font next to the exe under resources/font/
# ImGuiContext tries "resources/font/Inter.ttf" then "resources/font/Inter.ttc". # ImGuiContext tries "resources/font/Inter.ttf" then "resources/font/Inter.ttc".
install(FILES ${CMAKE_SOURCE_DIR}/third_party/Inter/Inter.ttc install(FILES ${CMAKE_SOURCE_DIR}/third_party/Inter/Inter.ttc
+4 -5
View File
@@ -9,15 +9,14 @@
"binaryDir": "${sourceDir}/build", "binaryDir": "${sourceDir}/build",
"cacheVariables": { "cacheVariables": {
"CMAKE_INSTALL_PREFIX": "${sourceDir}/install", "CMAKE_INSTALL_PREFIX": "${sourceDir}/install",
"CMAKE_PREFIX_PATH": "${sourceDir}/third_party/OpenUSD-v25.05", "CMAKE_PREFIX_PATH": "${sourceDir}/third_party/OpenUSD-v25.11",
"IMGUI_DIR": "${sourceDir}/third_party/imgui-1.92.7", "IMGUI_DIR": "${sourceDir}/third_party/imgui-1.92.7",
"CMAKE_CXX_STANDARD": "17", "CMAKE_CXX_STANDARD": "17",
"CMAKE_CXX_STANDARD_REQUIRED": "ON", "CMAKE_CXX_STANDARD_REQUIRED": "ON",
"BUILD_TESTS": "OFF", "BUILD_TESTS": "OFF",
"WITH_CYCLES": "ON", "WITH_CYCLES": "ON",
"HDARNOLD_ROOT": "E:/library/hdArnold", "HDARNOLD_ROOT": "E:/library/hdArnold-v25.11",
"ARNOLD_LOCATION": "C:/Autodesk/mtoa/5.5.0/2026", "ARNOLD_LOCATION": "C:/Autodesk/mtoa/5.5.0/2026"
"HDEMBREE_USD_ROOT": "E:/USD_v25.05_embree"
} }
}, },
{ {
@@ -36,7 +35,7 @@
"description": "Optimized release build", "description": "Optimized release build",
"inherits": "default", "inherits": "default",
"cacheVariables": { "cacheVariables": {
"CMAKE_BUILD_TYPE": "Release", "CMAKE_BUILD_TYPE": "RelWithDebInfo",
"BUILD_TESTS": "OFF" "BUILD_TESTS": "OFF"
} }
}, },
+40 -21
View File
@@ -1,26 +1,32 @@
# USD Layer Manager # USD Layer Manager
A C++17 / Windows desktop application providing a **Maya Render Layers-style** USD scene editor using **OpenUSD v25.05**, **ImGui 1.92.7** (docking branch), and **OpenGL 3.3** (via GLAD). Users can open, edit, and save USD files, manage layered stage overrides, inspect and edit prim properties, and interact with 3D scenes through a Hydra viewport. A C++17 / Windows desktop application providing a **Maya-style** USD scene and look-dev editor using **OpenUSD v25.11**, **ImGui 1.92.7** (docking branch), and **OpenGL 3.3** (via GLAD). Users can open, edit, and save USD files, manage layered stage overrides, inspect and edit prim properties, author UsdShade material graphs with a live shader-ball preview, animate with a Bezier curve editor, and interact with 3D scenes through a multi-viewport Hydra renderer with OCIO color management.
![Screenshot](docs/icons/screenshot_01.png) ![Screenshot](docs/icons/screenshot_01.png)
## Features ## Features
- **Layer Stack Management** — Create, reorder, mute/unmute sublayers with full undo support - **Layer Stack Management** — `StageEditorPanel`: create, reorder, mute/unmute sublayers, set the edit target, add existing files as sublayers, with full undo support
- **Scene Hierarchy** — Browse prim tree with per-type SVG icons; create, delete, rename prims; add/replace references - **Scene Hierarchy** — Browse prim tree with per-type SVG icons; create, delete, rename, reparent, and group prims; add/replace references
- **Property Editor** — Maya Channel Box-style attribute editing with type-aware drag inputs, color pickers, and undoable transforms - **Property Editor** — Maya Channel Box-style attribute editing with type-aware drag inputs, color pickers, and undoable transforms
- **3D Viewport** — Hydra rendering via `UsdImagingGLEngine` into an offscreen FBO; switchable render delegates (Storm, Embree, Arnold, Cycles); grid overlay; selection bounding boxes; camera wireframes - **Material Editor** — Hypershade-style node-graph editor for UsdShade/MaterialX networks (`MaterialEditorPanel`): material browser, drag-to-connect node canvas, per-node/per-pin display modes, Sdr-driven node creation menu and enum dropdowns, colorSpace authoring on texture inputs, and a live shader-ball preview (`MaterialPreviewRenderer`) with its own renderer/HDRI/preview-shape controls. In-canvas node thumbnails (`NodeThumbnailCache`) show texture previews and per-node shader-ball renders, amortized across frames and re-rendered until progressive delegates (Arnold/Cycles/Embree) fully converge
- **Multi-Viewport** — Single, horizontal split, vertical split, and quad layouts; per-tile camera and render settings; Space to maximize/restore - **Timeline & Curve Editor** — Transport controls with loop/bounce and Auto-Key (`TimelinePanel`); a Maya-style F-curve editor (`CurveEditorPanel`) with Bezier round-trip to USD time samples; Playblast export to MP4 via `MovieEncoder` (libavcodec)
- **3D Viewport** — Hydra rendering via `UsdImagingGLEngine` into an offscreen FBO; switchable render delegates (Storm, Embree, Arnold, Cycles); OCIO-based color management (sRGB or full display/view/look pipeline) applied as a post-Hydra GL pass; grid overlay; selection bounding boxes; camera/light wireframes
- **Multi-Viewport** — Single, horizontal split, vertical split, and quad layouts; per-tile camera, render delegate, and AOV; Space to maximize/restore
- **Camera Control** — Free orbital camera (tumble/truck/dolly) or drive from USD camera prims; auto near/far clipping; frame selection; correct camera prim orientation for Y-up and Z-up stages - **Camera Control** — Free orbital camera (tumble/truck/dolly) or drive from USD camera prims; auto near/far clipping; frame selection; correct camera prim orientation for Y-up and Z-up stages
- **Transform Gizmo** — Pure ImDrawList-based manipulator (translate/rotate/scale) in object or world space - **Transform Gizmo** — Pure ImDrawList-based manipulator (translate/rotate/scale) in object or world space
- **Undo/Redo** — Full command history for prim creation/deletion, attribute edits, transform changes, and layer operations; Ctrl+Z / Ctrl+Y hotkeys; Edit menu integration - **Undo/Redo** — Full command history (`ICommand` subclasses under `src/core/commands/`) covering prim creation/deletion/reparenting/grouping, attribute edits, transform changes, shader-node creation/connection, and layer operations; Ctrl+Z / Ctrl+Y hotkeys; Edit menu integration
- **Multi-select** — Rectangular selection in viewport with cross-panel syncing - **Multi-select** — Rectangular selection in viewport with cross-panel syncing
| Material Editor (Arnold) | Curve Editor | Quad Viewport (4 delegates) | MaterialX import |
|---|---|---|---|
| ![Material Editor](docs/icons/screenshot_06.png) | ![Curve Editor](docs/icons/screenshot_02.png) | ![Quad Viewport](docs/icons/screenshot_03.png) | ![MaterialX](docs/icons/screenshot_04.png) |
## Dependencies ## Dependencies
| Dependency | Version | Notes | | Dependency | Version | Notes |
|---|---|---| |---|---|---|
| [OpenUSD](https://github.com/PixarAnimationStudios/OpenUSD) | v25.05 | Prebuilt; found via `FindOpenUSD.cmake` | | [OpenUSD](https://github.com/PixarAnimationStudios/OpenUSD) | v25.11 | Prebuilt; found via `FindOpenUSD.cmake` |
| [Dear ImGui](https://github.com/ocornut/imgui) | v1.92.7 | Docking branch; Win32 + OpenGL3 backends | | [Dear ImGui](https://github.com/ocornut/imgui) | v1.92.7 | Docking branch; Win32 + OpenGL3 backends |
| [GLAD](https://glad.dav1d.de/) | — | OpenGL 3.3 core loader | | [GLAD](https://glad.dav1d.de/) | — | OpenGL 3.3 core loader |
| Python | 3.12 | Required by OpenUSD runtime (`python312.dll`) | | Python | 3.12 | Required by OpenUSD runtime (`python312.dll`) |
@@ -29,8 +35,8 @@ A C++17 / Windows desktop application providing a **Maya Render Layers-style** U
| Delegate | Renderer Version | Variable | Notes | | Delegate | Renderer Version | Variable | Notes |
|---|---|---|---| |---|---|---|---|
| **hdEmbree** | Embree 4.4.1 | `HDEMBREE_USD_ROOT` | USD build with `PXR_ENABLE_EMBREE_PLUGIN=ON`; set `EMBREE_LOCATION` if Embree DLLs are not in `HDEMBREE_USD_ROOT/bin` | | **hdEmbree** | Embree 4.3.3 | `HDEMBREE_USD_ROOT` (optional) | Bundled directly in `third_party/OpenUSD-v25.11` (built with `--embree`); set `HDEMBREE_USD_ROOT` only if using a separate USD build, and `EMBREE_LOCATION` if Embree DLLs aren't in its `bin/` |
| **hdArnold** | Arnold 7.4.0 (MtoA 5.5.0) | `HDARNOLD_ROOT`, `ARNOLD_LOCATION` | [arnold-usd](https://github.com/Autodesk/arnold-usd) install dir + MtoA root for `ai.dll` | | **hdArnold** | Arnold 7.4.0 (MtoA 5.5.0) | `HDARNOLD_ROOT`, `ARNOLD_LOCATION` | [arnold-usd](https://github.com/Autodesk/arnold-usd) (tag `Arnold-7.4.5.1`, for USD 25.11's `ndr``sdr` API change) install dir + MtoA root for `ai.dll` |
| **hdCycles** | Cycles 5.2.0 | `WITH_CYCLES=ON` | Built from source under `third_party/cycles` via ExternalProject; requires MSVC | | **hdCycles** | Cycles 5.2.0 | `WITH_CYCLES=ON` | Built from source under `third_party/cycles` via ExternalProject; requires MSVC |
All delegate paths are pre-configured in `CMakePresets.json`. All delegate paths are pre-configured in `CMakePresets.json`.
@@ -53,7 +59,7 @@ cmake --install build --config Release
The installed executable lives in `install/bin/`. The installed executable lives in `install/bin/`.
**Presets**`default` (Release + tests), `debug`, `release`, `no-tests`. **Presets**`default` (`Release`, `WITH_CYCLES=ON`, all render delegates), `debug` (`Debug`), `release` (`RelWithDebInfo`, installs), `no-tests` (`Release`). All presets currently build with `BUILD_TESTS=OFF`.
### Building with Cycles ### Building with Cycles
@@ -63,10 +69,7 @@ Cycles runtime dependencies (`embree4.dll`, `openvdb.dll`, `OpenImageDenoise.dll
### Tests ### Tests
```powershell There is currently no working test runner: `BUILD_TESTS` is `OFF` in every preset. The two test executables (`ViewportDisplayTest`, `RendererDiagnosticTest`) have stale USD include paths and don't build — fix those before turning `BUILD_TESTS` on.
cmake --build build --config Release
ctest --test-dir build -C Release
```
## Architecture ## Architecture
@@ -77,16 +80,23 @@ src/
│ ├── UsdStageManager — Stage open/create/save/close lifecycle │ ├── UsdStageManager — Stage open/create/save/close lifecycle
│ ├── LayerManager — Layer stack introspection and mutation │ ├── LayerManager — Layer stack introspection and mutation
│ ├── PropertyManager — Property read/write via edit target with undo │ ├── PropertyManager — Property read/write via edit target with undo
│ ├── MaterialManager — UsdShade graph introspection + Sdr node-type/enum discovery
│ ├── CommandHistory — Undo/redo stack for ICommand instances │ ├── CommandHistory — Undo/redo stack for ICommand instances
│ ├── UsdSceneRenderer — Hydra rendering + picking + overlays │ ├── UsdSceneRenderer — Hydra rendering + picking + overlays + OCIO color correction
│ ├── ViewportCamera — Free / USD-camera-driven orbital camera │ ├── ViewportCamera — Free / USD-camera-driven orbital camera
│ └── commands/ — ICommand subclasses for all mutable operations │ └── commands/ — ICommand subclasses for all mutable operations
│ ├── TransformCommand — Undoable translate/rotate/scale │ ├── TransformCommand — Undoable translate/rotate/scale
│ ├── CreatePrimCommand — Undoable prim creation (with camera orientation) │ ├── CreatePrimCommand — Undoable prim creation (with camera orientation)
│ ├── DeletePrimCommand — Undoable prim deletion (SdfCopySpec snapshot) │ ├── DeletePrimCommand — Undoable prim deletion (SdfCopySpec snapshot)
│ ├── RenamePrimCommand — Undoable prim rename
│ ├── ReparentPrimCommand — Undoable reparent via UsdNamespaceEditor
│ ├── GroupPrimsCommand — Group selected prims under a new Xform
│ ├── AttributeSetCommand — Type-agnostic undoable attribute writes │ ├── AttributeSetCommand — Type-agnostic undoable attribute writes
│ ├── AddReferenceCommand — Undoable reference addition │ ├── AddReferenceCommand — Undoable reference addition
│ ├── ReplaceReferenceCommand — Undoable reference replacement │ ├── ReplaceReferenceCommand — Undoable reference replacement
│ ├── CreateShaderNodeCommand — Undoable UsdShadeShader creation (Material Editor)
│ ├── ConnectShaderAttrsCommand — Undoable shader input↔output connection
│ ├── DisconnectShaderAttrCommand — Undoable shader connection removal
│ └── LayerCommands — Create, remove, reorder sublayers │ └── LayerCommands — Create, remove, reorder sublayers
├── ui/ — ImGui panels and application shell ├── ui/ — ImGui panels and application shell
│ ├── Application — App shell (owns managers/panels, docking, menus) │ ├── Application — App shell (owns managers/panels, docking, menus)
@@ -95,13 +105,21 @@ src/
│ ├── ViewportTile — Single viewport tile (camera, rendering, overlays, picking) │ ├── ViewportTile — Single viewport tile (camera, rendering, overlays, picking)
│ ├── SceneHierarchyPanel — Prim tree browser with context menus │ ├── SceneHierarchyPanel — Prim tree browser with context menus
│ ├── PropertyPanel — Attribute/transform editor (channel box-style) │ ├── PropertyPanel — Attribute/transform editor (channel box-style)
│ ├── LayerPanel — Layer stack editor with modal dialogs │ ├── StageEditorPanel — Layer stack editor (edit target, dirty state, drag-drop ordering)
│ ├── TimelinePanel — Transport controls, loop/bounce, frame scrub, Playblast
│ ├── CurveEditorPanel — Maya-style F-curve editor (Bezier ↔ USD time samples)
│ ├── MaterialEditorPanel — Hypershade-style UsdShade node-graph editor
│ ├── MaterialPreviewRenderer — Shader-ball preview: independent Hydra instance + scratch stage
│ ├── NodeThumbnailCache — Async texture decode + amortized shader-ball thumbnails for the node canvas
│ ├── TransformManipulator — ImDrawList gizmo (move/rotate/scale) │ ├── TransformManipulator — ImDrawList gizmo (move/rotate/scale)
── IconManager — SVG icon rasterization via NanoSVG ── IconManager — SVG icon rasterization via NanoSVG
│ └── NodeEditor/ — Vendored/patched imgui-node-editor
└── utils/ — Cross-cutting utilities └── utils/ — Cross-cutting utilities
├── Logger — Thread-safe logging (file + console) ├── Logger — Thread-safe logging (file + console); also sinks USD TF_WARN/TF_ERROR
├── FileDialog — Win32 file open/save dialogs ├── FileDialog — Win32 file open/save dialogs
├── PathUtils — Exe-relative path resolution ├── PathUtils — Exe-relative path resolution
├── OcioConfigParser — Enumerates displays/views/colorspaces/looks from the active OCIO config
├── MovieEncoder — Streams RGBA frames to H.264/MP4 via libavcodec (Playblast)
└── GLExt — GLAD extension initialization └── GLExt — GLAD extension initialization
``` ```
@@ -111,6 +129,7 @@ src/
- **Panel-Manager Separation** — UI panels hold pointers to core managers but contain no USD layer/prim logic - **Panel-Manager Separation** — UI panels hold pointers to core managers but contain no USD layer/prim logic
- **Callback Wiring** — Cross-panel communication via `std::function` callbacks (e.g. viewport pick → hierarchy select → property panel update) - **Callback Wiring** — Cross-panel communication via `std::function` callbacks (e.g. viewport pick → hierarchy select → property panel update)
- **Render-Delegate Plugability** — `UsdSceneRenderer` can switch Hydra render plugins at runtime - **Render-Delegate Plugability** — `UsdSceneRenderer` can switch Hydra render plugins at runtime
- **Own Color Correction Pass** — Hydra's `HdxColorCorrectionTask` is bypassed; a file-local `ViewportColorCorrector` (inside `UsdSceneRenderer.cpp`) applies sRGB/OCIO as a fullscreen GL pass after Hydra renders linear (see `docs/adr/0001-viewport-color-correction.md`)
- **Gizmo as 2D Overlay** — `TransformManipulator` uses pure `ImDrawList` calls (no GL resources), same approach as ImGuizmo - **Gizmo as 2D Overlay** — `TransformManipulator` uses pure `ImDrawList` calls (no GL resources), same approach as ImGuizmo
## Project Configuration ## Project Configuration
@@ -121,6 +140,7 @@ src/
| `cmake/modules/FindImgui.cmake` | ImGui source integration | | `cmake/modules/FindImgui.cmake` | ImGui source integration |
| `cmake/modules/FindGlad.cmake` | GLAD loader setup | | `cmake/modules/FindGlad.cmake` | GLAD loader setup |
| `CMakePresets.json` | Build presets (VS 17 2022, x64) | | `CMakePresets.json` | Build presets (VS 17 2022, x64) |
| `docs/adr/` | Architecture decision records |
| `AGENTS.md` | AI agent build/architecture guide | | `AGENTS.md` | AI agent build/architecture guide |
| `.kilo/` | Kilo AI configuration (commands, agents, skills) | | `.kilo/` | Kilo AI configuration (commands, agents, skills) |
| `openspec/changes/` | Feature proposals and implementation tracking | | `openspec/changes/` | Feature proposals and implementation tracking |
@@ -146,15 +166,14 @@ Use Kilo's `openspec-*` skills to streamline this workflow.
Before writing any OpenUSD API call, verify the API exists in the actual SDK: Before writing any OpenUSD API call, verify the API exists in the actual SDK:
```powershell ```powershell
findstr /r /s "FunctionName" third_party\OpenUSD_v25.05\include\ findstr /r /s "FunctionName" third_party\OpenUSD-v25.11\include\
``` ```
Always build and install before running tests: Always build and install before manually verifying a change:
```powershell ```powershell
cmake --build build --config Release cmake --build build --config Release
cmake --install build --config Release cmake --install build --config Release
ctest --test-dir build -C Release
``` ```
## License ## License
-1
View File
@@ -72,7 +72,6 @@ set(OpenUSD_REQUIRED_LIBS
hio hio
ar ar
kind kind
ndr
sdr sdr
python python
cameraUtil cameraUtil
+315
View File
@@ -0,0 +1,315 @@
# ADR 0002 — Render Layer 面板(Maya 風格,建構於 USD Sublayer 架構之上)
- **Status:** Proposed(尚未實作)
- **Date:** 2026-07-12
- **Component:** `src/core/RenderLayerManager`(新)、`src/ui/RenderLayerPanel`(新)、
`src/core/commands/RenderLayerCommands`(新)、
`src/core/commands/ConnectShaderAttrsCommand`
`src/core/commands/DisconnectShaderAttrCommand``src/ui/MaterialEditorPanel`
`src/core/UsdStageManager``src/ui/StageEditorPanel`
`src/ui/SceneHierarchyPanel``src/ui/Application`
- **Commit:** (尚未實作,無對應 commit)
## Context
使用者希望在本應用中加入一個「Render Layer」面板,行為比照 Maya 的
Render Layers:使用者可以把場景中的燈光與可渲染物件加入某個具名圖層,
該圖層記錄對這些物件的屬性調整——包含 shading 連線的修改——而不影響
場景的基礎資料;切換目前作用中的圖層時,viewport 套用該圖層的所有覆寫,
並且**只顯示該圖層的成員燈光與物件**;預設圖層(Default)則等同於目前
未經任何 render layer 修改的 base stage。
為了先弄清楚該怎麼設計,再動手實作,我們先派出三個平行的研究/探索
subagent:一個研究 Maya Render Layers(含舊版 Render Layers 與後續的
Render Setup)的實際資料模型與行為語意;一個深入探索本專案既有的
layer/stage/command 架構(`LayerManager``UsdStageManager`
`CommandHistory`、既有的 command 範本、`PropertyManager`
`StageEditorPanel`);一個探索 viewport 可見度控制與既有的材質覆寫機制
(`UsdSceneRenderer`/`UsdImagingGLEngine``SceneHierarchyPanel`
眼睛圖示隱藏機制、`MaterialEditorPanel` 的材質綁定與 shader 連線
command)。三份研究都完成後,再交由一個 Plan agent 讀取實際原始碼,
驗證並修正整體設計——這個過程中抓到兩個既有程式碼裡的真實正確性問題
(見下方 D3、D4)。本 ADR 記錄這輪研究與規劃後定案的設計決策。
**與真實 Maya 行為的刻意偏離:** Maya 切換 render layer 預設**不會**
自動 isolate viewport——那只是純粹的 render-time/override-time 概念,
非成員物件只有在該圖層明確加了 visibility override 時才會被隱藏。
但本專案的使用者明確要求「切換到圖層時,只顯示該圖層內的燈光與物件」,
這是經與使用者確認過的刻意設計,而非誤解 Maya 行為。
## Goals / Non-Goals
**Goals:**
- 非破壞性地記錄每個圖層的屬性覆寫(未來階段含 shading connection 與
material binding 覆寫),不修改場景的基礎資料。
- 切換作用中圖層時,viewport 即時反映該圖層的所有覆寫。
- 切換作用中圖層時,viewport 只顯示該圖層的成員燈光/物件(isolate)。
- 正確的存檔/重新開檔 round-trip(圖層清單、成員資格、覆寫內容、目前
作用中的圖層都要能還原)。
- 圖層切換與圖層內的編輯都要能正確 Undo/Redo。
**Non-Goals(本階段,MVP 範圍之外,留待後續 Phase):**
- Shading connection 覆寫的使用者介面(底層機制在 Phase 0 就會修正到
可正確運作,但本階段不在 Render Layer 面板上開放這個操作)。
- Material binding(材質指派)覆寫的使用者介面。
- 每圖層覆寫清單的檢視/還原(revert)UI,比照 Maya Render Setup 的
Property Editor 覆寫堆疊視圖。
- 從 Scene Hierarchy 拖放物件到 Render Layer 面板做成員編輯(MVP 先用
「加入所選」按鈕取代)。
這些項目會在後續 Phase(見 Consequences 與另一份實作計畫
`misty-watching-stardust.md`)補齊,此處先列出以留紀錄,避免日後誤以為
被遺漏。
## Decision
### D1 — 每個 Render Layer 對應一個獨立、檔案化的 `SdfLayer`
**決策:** 每建立一個 render layer,就建立一個實體(檔案化,而非
anonymous)的 `SdfLayer`,掛載為 stage root layer 的 sublayer,並以
`SdfLayer::SetCustomLayerData` 標記
(例如 `usdLayerManager:renderLayer = true`
`usdLayerManager:renderLayerName = "<名稱>"`),藉此與一般內容 sublayer
區分。絕對不可放進 session layer。
**理由:** `UsdStageManager::MergeSessionLayerIntoRoot()` 會在每次存檔
時把 session layer 的內容攤平合併進 root layer、再清空 session
layer——若 render layer 的資料放在 session layer,存檔當下就會被摧毀。
另外,anonymous `SdfLayer` 的 identifier(`anon:0x...`)只在執行期有效,
寫進 `subLayerPaths` 之後無法在 `SaveStageAs`/重新開檔時被正確解析,
因此每個 render layer 必須是真正的檔案(沿用
`LayerManager::CreateNewSublayer` 既有的「建立檔案 → 存檔 → 插入
sublayer」流程)。
---
### D2 — Default 圖層是虛擬的,沒有對應的 `SdfLayer`
**決策:** 「Default」不是一個真正的 render layer 物件,選取它只代表
「把所有 render-layer sublayer 全部靜音(mute)」。
**理由:** 這正好等同於今天應用程式既有的行為——沒有任何 render layer
生效時,viewport 顯示的就是未經修改的 base stage。不需要為 Default
另外設計一套資料結構或程式碼路徑。
---
### D3 — 成員資格以 `UsdCollectionAPI` 表示,並修正「非現用圖層無法讀寫」的問題
**決策:** 每個 render layer 的成員資格,用 `UsdCollectionAPI`
(instance name 固定為 `"members"`)表示,掛在該圖層自己 `SdfLayer`
內部一個固定保留路徑(`/RenderLayerData`)的 `over`-only admin prim
上。這個 prim 只有 `over`、沒有任何 `def`,所以是合法、可定址的
`UsdPrim`,但 `IsDefined() == false`,不會出現在
`UsdStage::Traverse()`、Scene Hierarchy 面板、或
`PropertyManager::GetPrimPaths()` 的預設列舉結果中,不需要額外過濾。
規劃過程中發現一個關鍵問題:由於同一時間只有一個 render layer 是
「現用」(unmuted)的,其餘全部處於 muted 狀態,而
`UsdStage::MuteLayer` 會讓該圖層**完全**退出 stage 的合成
(composition)——也就是說,對一個目前被 mute 的圖層呼叫
`stage->GetPrimAtPath("/RenderLayerData")` 會拿到一個**無效**的
`UsdPrim`,`Usd` 層級的 `UsdCollectionAPI` 便完全無法讀寫該圖層的成員
資料(例如使用者想在 Render Layer 面板編輯一個目前非現用圖層的成員
清單時)。
**修正做法:** 對於非現用圖層的成員資格讀寫,一律改走**Sdf 層級 API**,
直接對該圖層的 `SdfLayerHandle` 操作(`SdfCreatePrimInLayer`
建立/取得 `/RenderLayerData` 的 prim spec,再用 `SdfRelationshipSpec`
操作 `collection:members:includes` 這個 relationship 的
target path 清單)——這條路徑完全不經過 stage 合成,因此不受 mute
狀態影響,而且寫入的 opinion 名稱與 `UsdCollectionAPI` 完全一致。
只有在計算「目前現用圖層」的 isolate 用成員集合時,才使用一般
`Usd` 層級 API(`UsdCollectionAPI::GetCollection` +
`ComputeMembershipQuery` + `UsdComputeIncludedObjectsFromCollection`),
因為此時該圖層必然已經是 unmuted 的。兩條路徑存取的是同一份
authored opinion,只是兩種不同的入口,不是兩套 schema。
---
### D4 — 重用既有的 edit-target 機制做屬性覆寫,並修正三個既有的正確性缺陷
**決策:** 切換作用中的 render layer 時,把 stage 的
ambient edit target 指向該圖層的 `SdfLayer`(沿用
`LayerManager::SetEditTarget`)。
**理由與驗證:** 重新閱讀 `PropertyManager.cpp``TransformCommand`
後確認,`PropertyManager::SetPropertyValue`(Property Panel 一般屬性
編輯的路徑)與 `TransformCommand`(viewport 操作桿與 Property Panel
的 Transform 編輯路徑)都是在編輯提交的當下,從 stage 目前的
ambient edit target 解析要寫入哪個 `SdfLayer`。因此,只要把 ambient
edit target 換成 render layer 的 `SdfLayer`,這兩條既有的寫入路徑
**完全不需要修改任何程式碼**,就會自動把編輯正確記錄到對應的 render
layer 裡。
但在驗證過程中,發現另外三個既有的寫入路徑目前**沒有**捕捉/套用
`UsdEditContext`,而是在 `Execute()`/`Undo()` 真正執行的當下才讀取
ambient edit target:`ConnectShaderAttrsCommand`
`DisconnectShaderAttrCommand`、以及
`MaterialEditorPanel::BindMaterialToTarget` 的材質綁定/解除綁定
closure。具體會出錯的情境:使用者在 Render Layer A 生效時連接了一條
shader 連線(此時正確寫入 A);接著切換到 Render Layer B(ambient
edit target 也隨之換成 B);此時按下復原(Undo),`Undo()` 會在
**B** 這個圖層上執行「移除連線」,而不是回到原本執行 `Execute()`
時的 A——結果是 A 留下一條沒被正確復原的連線 opinion,B 卻多了一條
不該存在的覆寫。這其實是既有程式碼中已經存在、但目前很少被觸發到的
潛在 bug(任何在「連線」與「復原連線」之間發生 edit target 變更的
情境都會中招),必須在導入 render layer 之前先修正,否則問題會被
render layer 的切換行為放大成使用者能輕易踩到的日常錯誤。
**修正做法:** 比照 `TransformCommand` 既有的寫法,為
`ConnectShaderAttrsCommand``DisconnectShaderAttrCommand`
新增一個建構參數 `SdfLayerHandle editLayer`,並在 `Execute()`/
`Undo()` 內以 `UsdEditContext(m_stage, m_editLayer)`
包裹實際的連線/解除連線呼叫;`MaterialEditorPanel` 在建立這些
command 與綁定/解除綁定的 closure 時,一併在建構當下捕捉
`stage->GetEditTarget().GetLayer()` 並傳入。這個修正不影響現有
(尚無 render layer 時)的行為——沒有明確傳入圖層時,維持原本讀取
ambient edit target 的行為不變。
---
### D5 — Viewport Isolate 採用「authored visibility opinion」,而非重建渲染引擎或整個 stage 的 population mask
**決策:** 圖層啟用、或該圖層成員資格變動時,對場景做一次由上而下的
遍歷:若某個子樹完全不含任何成員,就在該子樹**最上層**的 prim 上,
`UsdGeomImageable`(與 Scene Hierarchy 面板既有的眼睛圖示隱藏
機制完全相同的原語)authoring 一個 `visibility = invisible`,並且
**停止往下遞迴**(子孫節點靠繼承取得隱藏效果);若子樹內含成員,則不
寫入任何東西、繼續往下遞迴。規則上永遠不主動 authoring
`visible`——只利用預設的「inherited」語意,避免蓋掉場景中原本就刻意
隱藏的物件。所有自動寫入的路徑,記錄在該圖層自己的
`customLayerData`(例如 `usdLayerManager:autoHiddenPaths`)中,
以便下次重新計算前可以精準只清除這些自動產生的 opinion,不誤刪
使用者手動authoring 的覆寫。
**理由(排除的替代方案見下方 Alternatives Considered):**
重新閱讀
`third_party/OpenUSD-v25.05/include/pxr/usdImagingGL/engine.h`
確認,`UsdImagingGLEngine::Parameters::invisedPaths`
只能在**建構時**指定,整個類別沒有任何執行期可用的
setter——每次切換圖層都重建 engine 的代價太高(會丟失 Hydra
scene-index 狀態,以及 Arnold/Cycles/Embree 等漸進式渲染委派已累積
的取樣結果,並造成 viewport 明顯的畫面閃爍)。而
`UsdStage::SetPopulationMask()`/`SetLoadRules()`
雖然可在執行期呼叫,但作用範圍是整個 stage 的合成,會連帶讓
Scene Hierarchy 面板、Property Panel 的選取、viewport 的
pick 都看不到/選不到非成員物件——這遠超出「只讓 viewport
isolate」的需求,等於把物件從整個場景圖裡「拿掉」而非「隱藏」。
以真正的 authored opinion 達成 isolate,則是可檢視、可除錯的
USD 資料(能在 Stage Editor 等既有工具中直接看到),而且已經證實
Hydra 能對這類編輯即時反應而不需要重建 engine(既有的 Property
Panel 編輯、Scene Hierarchy 的眼睛圖示隱藏,都是同一套機制)。
開銷與被隱藏的「子樹根節點」數量成正比,而非與場景總物件數成正比,
對一般場景規模而言可忽略。
---
### D6 — 圖層切換本身也是一個可 Undo 的操作
**決策:** 新增 `RenderLayerActivateCommand`,和其他編輯一樣推入
`CommandHistory`。建構時就先記錄「切換前的 edit target」,以便
Undo 或切回 Default 時正確還原。
**理由:** 切換圖層會 authoring 真正的狀態(isolate 用的 visibility
opinion、edit target 的變更),如果不納入 Undo 堆疊,使用者連續按
Ctrl+Z 時會「跳過」圖層切換這一步,卻仍然復原了在該圖層內做的編輯,
造成不一致、難以理解的復原歷史。此設計已與使用者確認。
---
### D7 — 圖層生效期間,鎖定既有的 edit-target 相關 UI
**決策:** 當某個非 Default 的 render layer 是現用狀態時,停用
Stage Editor 面板的 edit-target checkbox,以及 Scene Hierarchy
面板的圖層下拉選單,並加上提示文字說明原因。
**理由:** 這兩個既有的 UI 都是直接讀寫 stage 的 ambient edit
target。如果放任使用者在 render layer 生效期間透過這些既有 UI
把 edit target 改到別的地方,「編輯會正確落在目前作用中的 render
layer」這個核心不變量就會被悄悄破壞,且沒有任何提示。鎖定雖然犧牲
一些彈性,但能避免使用者在不知情的狀況下把覆寫寫錯地方。此設計已
與使用者確認(相對於「保留可操作、只顯示警告」的替代方案)。
---
### D8 — 存檔與重新開檔的持久化
**決策:** `UsdStageManager::SaveStage`/`SaveStageAs`
除了既有的存檔呼叫之外,額外明確逐一存下**每一個** render-layer
`SdfLayer`(不論目前是否被 mute)。哪個圖層目前是「現用」這件事,
額外以 root layer 的 custom layer data 記錄
(例如 `usdLayerManager:activeRenderLayer`),並在
`Application::RefreshManagers()`/開啟 stage 時讀回,重新套用對應的
`MuteLayer`/`UnmuteLayer``SetEditTarget`
**理由:** `UsdStage::Save()`
只會走訪目前在合成(composition)中的 layer 存檔,而同一時間必然有
N-1 個 render-layer sublayer 處於 muted、不在合成中的狀態——若不
額外處理,這些圖層的編輯內容在存檔時會被靜默遺漏。另外,mute 狀態
本身只是 `UsdStage` 的執行期旗標,並不會被序列化進任何檔案,因此
「目前是哪個圖層生效」這件事也必須另外顯式持久化,否則重新開檔後
會遺失,退回成 Default 生效的狀態。
## Consequences
**正面:**
- 幾乎完全重用既有的 `SdfLayer`/`UsdEditContext`/`CommandHistory`/
`LayerManager` 機制,不需要引入新的渲染或合成機制,實作與既有
程式碼風格高度一致。
- 每個圖層的覆寫都是真正、可檢視的 USD opinion,能在 Stage Editor
等既有工具中直接檢視、除錯,不是一個只存在於應用程式記憶體裡的
「影子」狀態。
- Isolate 邏輯的開銷與「被隱藏的子樹根節點數」成正比,不隨場景總
物件數線性增加。
- 圖層切換、圖層內編輯,都能正確納入既有的 Undo/Redo 體系,使用者
操作心智模型一致。
**負面/取捨:**
- 成員資格需要維護兩條存取路徑(現用圖層走 `Usd` 層級 API、非現用
圖層走 `Sdf` 層級 API),增加一些實作複雜度與日後維護成本。
- 鎖定既有 edit-target UI 犧牲了一部分操作彈性。
- 每個 render layer 都對應一個實體檔案(而非 anonymous layer),
會在專案目錄下產生額外的附屬檔案,需要一致的檔名/存放規則。
- 本階段(MVP)尚未支援 shading connection、material binding 的
每圖層覆寫 UI,也還沒有覆寫清單的檢視/還原介面——這些留待後續
Phase,在此之前 Render Layer 面板的覆寫能力僅限一般屬性數值與
Transform。
- `ConnectShaderAttrsCommand`/`DisconnectShaderAttrCommand`/
`BindMaterialToTarget` 的修正屬於既有程式碼的行為調整,雖然設計上
刻意保持向下相容(未傳入圖層時行為不變),仍需要在導入 render
layer 之前完成並個別驗證,不能與 render layer 本身的功能驗證
混在一起。
## Alternatives Considered
- **用 `UsdStage::SetPopulationMask()`/`SetLoadRules()` 做 isolate。**
否決:這兩者作用在整個 stage 的合成層級,會讓 Scene Hierarchy
面板、Property Panel 的選取與 viewport 的 pick 一併看不到/選不到
非成員物件,遠超出「只讓 viewport isolate」的實際需求。
- **用 `UsdImagingGLEngine` 建構期的 `invisedPaths`,每次切換圖層就
重建 engine。** 否決:代價過高——會丟失 Hydra scene-index 狀態,
以及漸進式渲染委派(Arnold/Cycles/Embree)已累積的取樣結果,並造成
viewport 明顯閃爍,不適合互動式的頻繁圖層切換。
- **用 anonymous `SdfLayer` 存放每個 render layer 的資料。**
否決:anonymous layer 的 identifier 只在執行期有效,寫進
`subLayerPaths` 後,在 `SaveStageAs`/重新開檔時無法被正確解析,
資料會在存檔/重載之間遺失。
- **用 `UsdVariantSet` 表示 render layer 之間互斥的選擇。**
否決:Maya 的 collection + override 模型本質上更接近可疊加、
可獨立排序的 sublayer/override 合成,而非彼此互斥、一次只能選一
個的 variant 切換,語意上不夠貼合(尤其考慮到未來允許同一物件
同時屬於多個圖層的情境)。
## Verification
本 ADR 記錄的是**實作前**的設計決策——目前尚未進行任何程式碼變更,
因此本節暫不回報建置或手動驗證結果。待對應的實作計畫
(`misty-watching-stardust.md` 中記錄的 Phase 0/Phase 1 MVP)實際
執行完成後,應在本節或另外新增一則後續紀錄中,補上實際的建置結果與
手動驗證步驟/結果(圖層建立與切換、viewport isolate 是否正確、
屬性覆寫是否正確落在對應圖層且不外洩到其他圖層、存檔重新開檔的
round-trip、跨圖層切換的 Undo/Redo 行為),寫法比照
`0001-viewport-color-correction.md` 文末的「Supersession Note」
——以追加的方式記錄後續進展或設計變動,而非直接覆寫、抹除本次
決策當下的紀錄。
Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M41.4 233.4c12.5-12.5 32.8-12.5 45.3 0L256 402.7 425.4 233.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0l-192-192c-12.5-12.5-12.5-32.8 0-45.3z"/></svg>

After

Width:  |  Height:  |  Size: 471 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M233.4 105.4c12.5-12.5 32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 173.3 86.6 342.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l192-192z"/></svg>

After

Width:  |  Height:  |  Size: 460 B

+39 -1
View File
@@ -1 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c8.4-19.3 10.6-41.4 4.8-63.3c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zM373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5L373 389.9z"/></svg> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="eye-slash.svg"
width="512"
height="512"
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.7441406"
inkscape:cx="319.92833"
inkscape:cy="256"
inkscape:window-width="1920"
inkscape:window-height="1137"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="M 45.154688,63.56677 C 37.354061,57.416274 26.02815,58.841389 19.877654,66.642017 13.727159,74.442646 15.152273,85.768558 22.952903,91.919053 L 466.98865,439.94706 c 7.80061,6.1505 19.12653,4.72539 25.27703,-3.07524 6.15049,-7.80063 4.72538,-19.12654 -3.07524,-25.27704 l -78.90636,-61.80497 c 29.7024,-30.45246 49.80401,-64.5802 59.92982,-88.80716 2.4752,-5.92547 2.4752,-12.526 0,-18.45147 -11.17589,-26.77716 -34.65278,-65.7803 -69.75561,-98.33292 -35.25284,-32.85264 -83.78174,-60.454866 -144.38662,-60.454866 -51.15412,0 -93.75756,19.726586 -126.98523,45.603666 z M 183.39082,171.87549 c 19.12653,-17.47641 44.70359,-28.12727 72.68085,-28.12727 59.6298,0 108.0087,48.37889 108.0087,108.0087 0,18.6765 -4.72539,36.22791 -13.05107,51.52914 l -28.95232,-22.65182 c 6.30051,-14.47616 7.95063,-31.05249 3.60029,-47.47882 -8.32567,-31.1275 -35.85288,-52.05419 -66.45536,-53.3293 -4.35034,-0.15001 -6.90055,4.57537 -5.55044,8.77572 1.57513,4.80038 2.4752,9.90078 2.4752,15.22622 0,7.65061 -1.80014,14.85119 -4.9504,21.22671 L 183.46581,171.95049 Z M 295.82487,352.19 c -12.30099,4.87539 -25.72707,7.57562 -39.7532,7.57562 -59.62981,0 -108.0087,-48.3789 -108.0087,-108.0087 0,-5.17542 0.37503,-10.20082 1.05009,-15.15122 L 78.382365,180.87621 c -17.101377,22.27679 -29.327363,44.47858 -36.452936,61.65497 -2.475199,5.92547 -2.475199,12.526 0,18.45147 11.175899,26.77717 34.652789,65.7803 69.755611,98.33293 35.25284,32.85264 83.78174,60.45487 144.38663,60.45487 35.85289,0 67.43042,-9.67578 94.65761,-24.37697 z"
id="path1"
style="stroke-width:0.75006" />
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

+39 -1
View File
@@ -1 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z"/></svg> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="eye.svg"
width="512"
height="512"
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.7441406"
inkscape:cx="287.82083"
inkscape:cy="256"
inkscape:window-width="1920"
inkscape:window-height="1137"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="m 250.94921,80.734602 c -63.04963,0 -113.53614,28.715668 -150.28908,62.893558 -36.518844,33.86576 -60.942769,74.44226 -72.491463,102.29957 -2.575046,6.16451 -2.575046,13.0313 0,19.1958 11.548694,27.85732 35.972619,68.43382 72.491463,102.29958 36.75294,34.17789 87.23945,62.89356 150.28908,62.89356 63.04962,0 113.53613,-28.71567 150.28907,-62.89356 36.51884,-33.94379 60.94277,-74.44226 72.56949,-102.29958 2.57505,-6.1645 2.57505,-13.03129 0,-19.1958 C 462.18105,218.07042 437.75712,177.49392 401.23828,143.62816 364.48534,109.45027 313.99883,80.734602 250.94921,80.734602 Z M 138.58355,255.52563 a 112.36566,112.36566 0 1 1 224.73132,0 112.36566,112.36566 0 1 1 -224.73132,0 z m 112.36566,-49.94029 c 0,27.54519 -22.39511,49.94029 -49.9403,49.94029 -5.54025,0 -10.84641,-0.93637 -15.84043,-2.57505 -4.29175,-1.40456 -9.28578,1.24851 -9.12972,5.77436 0.2341,5.38418 1.01441,10.76837 2.49702,16.15256 10.69034,39.95223 51.81305,63.67387 91.76529,52.98353 39.95224,-10.69034 63.67387,-51.81305 52.98353,-91.76529 -8.66152,-32.38316 -37.29916,-54.15401 -69.13609,-55.48055 -4.52584,-0.15607 -7.17892,4.75993 -5.77435,9.12971 1.63866,4.99403 2.57505,10.30019 2.57505,15.84044 z"
id="path1"
style="stroke-width:0.780317" />
</svg>

Before

Width:  |  Height:  |  Size: 863 B

After

Width:  |  Height:  |  Size: 2.4 KiB

+39 -1
View File
@@ -1 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM216 232l0 48 48 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-48 0 0 48c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-48-48 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l48 0 0-48c0-13.3 10.7-24 24-24s24 10.7 24 24z"/></svg> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 64 64"
version="1.1"
id="svg1"
sodipodi:docname="file-plus.svg"
width="64"
height="64"
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="13.953125"
inkscape:cx="30.351624"
inkscape:cy="32.788354"
inkscape:window-width="1920"
inkscape:window-height="1137"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="m 18.366884,5.8051512 c -3.568558,0 -6.469907,2.9085663 -6.469907,6.4860018 v 38.916014 c 0,3.577435 2.901349,6.486002 6.469907,6.486002 h 25.879632 c 3.568559,0 6.469908,-2.908567 6.469908,-6.486002 V 22.020157 H 37.776609 c -1.789335,0 -3.234954,-1.449217 -3.234954,-3.243002 V 5.8051512 Z m 19.409725,0 V 18.777155 H 50.716424 Z M 33.732916,29.316909 v 4.864502 h 4.852431 c 1.344528,0 2.426216,1.084379 2.426216,2.43225 0,1.347873 -1.081688,2.432252 -2.426216,2.432252 h -4.852431 v 4.864501 c 0,1.347872 -1.081688,2.43225 -2.426216,2.43225 -1.344527,0 -2.426215,-1.084378 -2.426215,-2.43225 v -4.864501 h -4.852431 c -1.344528,0 -2.426215,-1.084379 -2.426215,-2.432252 0,-1.347871 1.081687,-2.43225 2.426215,-2.43225 h 4.852431 v -4.864502 c 0,-1.347872 1.081688,-2.43225 2.426215,-2.43225 1.344528,0 2.426216,1.084378 2.426216,2.43225 z"
id="path1"
style="stroke-width:0.101217" />
</svg>

Before

Width:  |  Height:  |  Size: 634 B

After

Width:  |  Height:  |  Size: 2.1 KiB

+37 -1
View File
@@ -1 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M0 32C0 14.3 14.3 0 32 0L160 0c17.7 0 32 14.3 32 32l0 384c0 53-43 96-96 96s-96-43-96-96L0 32zM223.6 425.9c.3-3.3 .4-6.6 .4-9.9l0-262 75.4-75.4c12.5-12.5 32.8-12.5 45.3 0l90.5 90.5c12.5 12.5 12.5 32.8 0 45.3L223.6 425.9zM182.8 512l192-192L480 320c17.7 0 32 14.3 32 32l0 128c0 17.7-14.3 32-32 32l-297.2 0zM128 64L64 64l0 64 64 0 0-64zM64 192l0 64 64 0 0-64-64 0zM96 440a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"/></svg> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 512 512"
version="1.1"
id="svg1"
sodipodi:docname="swatchbook.svg"
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.7441406"
inkscape:cx="255.71333"
inkscape:cy="256"
inkscape:window-width="1920"
inkscape:window-height="1137"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path
d="m 36.12094,73.137738 c 0,-14.449385 12.282308,-26.12318 27.484883,-26.12318 H 173.54535 c 15.20258,0 27.48489,11.673795 27.48489,26.12318 V 386.6159 c 0,43.26652 -36.93281,78.36954 -82.45465,78.36954 -45.521839,0 -82.45465,-35.10302 -82.45465,-78.36954 z M 228.17156,394.69776 c 0.25766,-2.69395 0.34356,-5.38791 0.34356,-8.08186 V 172.73237 l 64.76125,-61.55275 c 10.73629,-10.20437 28.172,-10.20437 38.90828,0 l 77.7307,73.87962 c 10.73629,10.20436 10.73629,26.77626 0,36.98062 z m -35.04323,70.28768 164.9093,-156.73908 h 90.35655 c 15.20257,0 27.48488,11.6738 27.48488,26.12318 v 104.49273 c 0,14.44938 -12.28231,26.12317 -27.48488,26.12317 z M 146.06047,99.260915 H 91.090705 V 151.50728 H 146.06047 Z M 91.090705,203.75364 V 256 h 54.969765 v -52.24636 z m 27.484885,202.45465 a 20.613662,19.592386 0 1 0 0,-39.18477 20.613662,19.592386 0 1 0 0,39.18477 z"
id="path1"
style="stroke-width:0.837356" />
</svg>

Before

Width:  |  Height:  |  Size: 693 B

After

Width:  |  Height:  |  Size: 2.1 KiB

+14 -2
View File
@@ -138,6 +138,7 @@ ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& material
if (!materialPrim.IsValid()) return snapshot; if (!materialPrim.IsValid()) return snapshot;
static const pxr::TfToken kUiPositionKey("uiPosition"); static const pxr::TfToken kUiPositionKey("uiPosition");
static const pxr::TfToken kUiPinDisplayModeKey("uiPinDisplayMode");
// Seed with every shader under the material, recursing into nested node // Seed with every shader under the material, recursing into nested node
// graphs (usdMtlx nests a material's nodes inside UsdShadeNodeGraph // graphs (usdMtlx nests a material's nodes inside UsdShadeNodeGraph
@@ -179,6 +180,13 @@ ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& material
node.hasAuthoredPosition = true; node.hasAuthoredPosition = true;
} }
pxr::VtValue pinModeValue = child.GetCustomDataByKey(kUiPinDisplayModeKey);
if (pinModeValue.IsHolding<int>()) {
int raw = pinModeValue.UncheckedGet<int>();
if (raw >= static_cast<int>(PinDisplayMode::All) && raw <= static_cast<int>(PinDisplayMode::Collapsed))
node.pinDisplayMode = static_cast<PinDisplayMode>(raw);
}
// Prefer the full Sdr-defined pin set (so unauthored pins can still be // Prefer the full Sdr-defined pin set (so unauthored pins can still be
// dragged to create a connection); fall back to authored attributes // dragged to create a connection); fall back to authored attributes
// only for shader types the registry doesn't know about. // only for shader types the registry doesn't know about.
@@ -187,8 +195,12 @@ ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& material
if (sdrNode) { if (sdrNode) {
node.category = DeriveCategory(sdrNode); node.category = DeriveCategory(sdrNode);
for (const pxr::TfToken& inputName : sdrNode->GetShaderInputNames()) { for (const pxr::TfToken& inputName : sdrNode->GetShaderInputNames()) {
if (auto* prop = sdrNode->GetShaderInput(inputName)) if (auto* prop = sdrNode->GetShaderInput(inputName)) {
node.inputs.push_back({inputName.GetString(), prop->GetTypeAsSdfType().GetSdfType()}); ShaderPinInfo pin{inputName.GetString(), prop->GetTypeAsSdfType().GetSdfType()};
for (const auto& option : prop->GetOptions())
pin.allowedValues.push_back(option.first.GetString());
node.inputs.push_back(std::move(pin));
}
} }
for (const pxr::TfToken& outputName : sdrNode->GetShaderOutputNames()) { for (const pxr::TfToken& outputName : sdrNode->GetShaderOutputNames()) {
if (auto* prop = sdrNode->GetShaderOutput(outputName)) if (auto* prop = sdrNode->GetShaderOutput(outputName))
+18
View File
@@ -31,6 +31,21 @@ struct ShaderNodeTypeInfo {
struct ShaderPinInfo { struct ShaderPinInfo {
std::string name; std::string name;
pxr::SdfValueTypeName typeName; pxr::SdfValueTypeName typeName;
/// Sdr-declared allowed values (e.g. UsdUVTexture's sourceColorSpace:
/// "raw"/"sRGB"/"auto"), empty when the property isn't enum-constrained.
/// Drives a dropdown instead of a free-text field in the property panel.
std::vector<std::string> allowedValues;
};
/// Hypershade-style pin display mode for a node in the graph canvas.
/// All: every Sdr-defined pin shown (today's default). Connected: only
/// linked pins shown, the rest collapsed behind a single connectable nub per
/// side. Collapsed: every pin — linked or not — collapses behind that one
/// nub per side, so existing connections visually converge on it too.
enum class PinDisplayMode {
All = 0,
Connected = 1,
Collapsed = 2,
}; };
/// One UsdShadeShader prim read back from an existing material network. /// One UsdShadeShader prim read back from an existing material network.
@@ -48,6 +63,9 @@ struct ShaderGraphNode {
/// False when no uiPosition custom data is authored (typical for networks /// False when no uiPosition custom data is authored (typical for networks
/// referenced from .mtlx) — the editor auto-lays such nodes out instead. /// referenced from .mtlx) — the editor auto-lays such nodes out instead.
bool hasAuthoredPosition = false; bool hasAuthoredPosition = false;
/// Pin display mode, read from customData when the "keep graph node view
/// settings in USD" preference authored it; All when unauthored.
PinDisplayMode pinDisplayMode = PinDisplayMode::All;
std::vector<ShaderPinInfo> inputs; std::vector<ShaderPinInfo> inputs;
std::vector<ShaderPinInfo> outputs; std::vector<ShaderPinInfo> outputs;
}; };
+456
View File
@@ -0,0 +1,456 @@
#include "RenderLayerManager.h"
#include "LayerManager.h"
#include "../utils/Logger.h"
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/usd/editTarget.h>
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/sdf/primSpec.h>
#include <pxr/usd/sdf/relationshipSpec.h>
#include <pxr/usd/sdf/proxyTypes.h>
#include <pxr/usd/usdGeom/imageable.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/base/vt/dictionary.h>
#include <pxr/base/vt/array.h>
#include <pxr/base/tf/token.h>
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <functional>
#include <set>
PXR_NAMESPACE_USING_DIRECTIVE
namespace UsdLayerManager {
namespace {
// Reserved, undefined ("over"-only) admin prim path each render layer's own
// SdfLayer carries its membership relationship on. Never a `def`, so it never
// shows up in UsdStage::Traverse() / the Scene Hierarchy panel / GetChildren().
const SdfPath kAdminPrimPath("/RenderLayerData");
const TfToken kMembersIncludesName("collection:members:includes");
// SdfLayer custom-layer-data keys.
const std::string kMarkerKey = "usdLayerManager:renderLayer";
const std::string kNameKey = "usdLayerManager:renderLayerName";
const std::string kAutoHiddenKey = "usdLayerManager:autoHiddenPaths";
// Root-layer-only key: persists which render layer was active across save/reload.
const std::string kActiveLayerKey = "usdLayerManager:activeRenderLayer";
std::string SanitizeFileStem(const std::string& raw) {
std::string result;
result.reserve(raw.size());
for (char c : raw) {
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '-')
result += c;
else
result += '_';
}
return result;
}
SdfPath MembersIncludesPath() {
return kAdminPrimPath.AppendProperty(kMembersIncludesName);
}
} // namespace
RenderLayerManager::RenderLayerManager() {}
RenderLayerManager::~RenderLayerManager() {}
void RenderLayerManager::SetStage(UsdStageRefPtr stage) {
m_stage = stage;
Refresh();
}
void RenderLayerManager::Refresh() {
BuildList();
}
void RenderLayerManager::BuildList() {
m_layers.clear();
m_layerRefs.clear();
RenderLayerInfo def;
def.name = "Default";
def.isDefault = true;
m_layers.push_back(def);
if (!m_stage) return;
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return;
bool anyActive = false;
for (const auto& subPath : rootLayer->GetSubLayerPaths()) {
SdfLayerRefPtr sub = SdfLayer::FindOrOpenRelativeToLayer(rootLayer, subPath);
if (!sub) continue;
VtDictionary cld = sub->GetCustomLayerData();
auto markerIt = cld.find(kMarkerKey);
if (markerIt == cld.end() || !markerIt->second.IsHolding<bool>() ||
!markerIt->second.Get<bool>())
continue; // not a render layer sublayer — leave for LayerManager/StageEditorPanel
m_layerRefs.push_back(sub);
RenderLayerInfo info;
info.layer = SdfLayerHandle(sub);
info.layerIdentifier = sub->GetIdentifier();
auto nameIt = cld.find(kNameKey);
info.name = (nameIt != cld.end() && nameIt->second.IsHolding<std::string>())
? nameIt->second.Get<std::string>()
: LayerManager::ExtractDisplayName(info.layerIdentifier);
info.isActive = !m_stage->IsLayerMuted(info.layerIdentifier);
if (info.isActive) anyActive = true;
m_layers.push_back(info);
}
m_layers[0].isActive = !anyActive;
}
/*static*/ bool RenderLayerManager::IsRenderLayerSublayer(const SdfLayerHandle& layer) {
if (!layer) return false;
VtDictionary cld = layer->GetCustomLayerData();
auto it = cld.find(kMarkerKey);
return it != cld.end() && it->second.IsHolding<bool>() && it->second.Get<bool>();
}
void RenderLayerManager::RefreshLayerManager() {
if (m_layerManager) m_layerManager->Refresh();
}
SdfLayerHandle RenderLayerManager::FindRenderLayer(const std::string& layerId) const {
for (const auto& info : m_layers)
if (!info.isDefault && info.layerIdentifier == layerId)
return info.layer;
return SdfLayerHandle();
}
// ---------------------------------------------------------------------------
// CRUD
// ---------------------------------------------------------------------------
std::string RenderLayerManager::CreateRenderLayer(const std::string& name) {
if (!m_stage) return {};
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return {};
namespace fs = std::filesystem;
fs::path baseDir;
std::string realPath = rootLayer->GetRealPath();
baseDir = !realPath.empty()
? fs::path(realPath).parent_path() / ".renderlayers"
: fs::current_path() / ".renderlayers";
std::error_code ec;
fs::create_directories(baseDir, ec);
std::string stem = SanitizeFileStem(name.empty() ? "RenderLayer" : name);
if (stem.empty()) stem = "RenderLayer";
std::string finalStem = stem;
fs::path filePath = baseDir / (finalStem + ".usda");
int suffix = 1;
while (fs::exists(filePath)) {
finalStem = stem + "_" + std::to_string(suffix++);
filePath = baseDir / (finalStem + ".usda");
}
SdfLayerRefPtr newLayer = SdfLayer::CreateNew(filePath.string());
if (!newLayer) {
LOG_ERROR("RenderLayerManager: failed to create layer file: " + filePath.string());
return {};
}
VtDictionary cld;
cld[kMarkerKey] = VtValue(true);
cld[kNameKey] = VtValue(name.empty() ? finalStem : name);
newLayer->SetCustomLayerData(cld);
SdfPrimSpecHandle adminPrim = SdfCreatePrimInLayer(newLayer, kAdminPrimPath);
if (adminPrim)
SdfRelationshipSpec::New(adminPrim, kMembersIncludesName.GetString(),
/*custom=*/false, SdfVariabilityUniform);
newLayer->Save();
rootLayer->InsertSubLayerPath(filePath.string(), 0);
m_stage->MuteLayer(filePath.string()); // starts inactive until explicitly activated
Refresh();
RefreshLayerManager();
return newLayer->GetIdentifier();
}
bool RenderLayerManager::DeleteRenderLayer(const std::string& layerId) {
if (!m_stage) return false;
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return false;
if (!FindRenderLayer(layerId)) return false;
if (GetActiveRenderLayerId() == layerId)
ActivateDefaultLayer();
SdfSubLayerProxy subPaths = rootLayer->GetSubLayerPaths();
for (size_t i = 0; i < subPaths.size(); ++i) {
SdfLayerRefPtr sub = SdfLayer::FindOrOpenRelativeToLayer(rootLayer, subPaths[i]);
if (sub && sub->GetIdentifier() == layerId) {
subPaths.erase(subPaths.begin() + static_cast<long>(i));
Refresh();
RefreshLayerManager();
return true;
}
}
return false;
}
bool RenderLayerManager::RestoreDeletedRenderLayer(const std::string& layerIdentifier) {
if (!m_stage) return false;
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return false;
rootLayer->InsertSubLayerPath(layerIdentifier, 0);
Refresh();
RefreshLayerManager();
return true;
}
bool RenderLayerManager::RenameRenderLayer(const std::string& layerId, const std::string& newName) {
SdfLayerHandle layer = FindRenderLayer(layerId);
if (!layer || newName.empty()) return false;
VtDictionary cld = layer->GetCustomLayerData();
cld[kNameKey] = VtValue(newName);
layer->SetCustomLayerData(cld);
Refresh();
RefreshLayerManager();
return true;
}
// ---------------------------------------------------------------------------
// Switching
// ---------------------------------------------------------------------------
bool RenderLayerManager::ActivateRenderLayer(const std::string& layerId) {
if (!m_stage) return false;
SdfLayerHandle target = FindRenderLayer(layerId);
if (!target) return false;
// First entry into render-layer mode this "session" (Default -> a render
// layer): remember the edit target so ActivateDefaultLayer() can restore
// it later, no matter how many render-layer-to-render-layer switches
// happen in between.
if (IsDefaultActive()) {
SdfLayerHandle currentEt = m_stage->GetEditTarget().GetLayer();
m_savedEditTargetIdBeforeMode = currentEt ? currentEt->GetIdentifier() : std::string();
}
for (const auto& info : m_layers) {
if (info.isDefault || info.layerIdentifier == layerId) continue;
if (!m_stage->IsLayerMuted(info.layerIdentifier))
m_stage->MuteLayer(info.layerIdentifier);
}
if (m_stage->IsLayerMuted(layerId))
m_stage->UnmuteLayer(layerId);
if (m_layerManager) m_layerManager->SetEditTarget(layerId);
else m_stage->SetEditTarget(UsdEditTarget(target));
RecomputeIsolation(target, layerId);
PersistActiveLayerId(layerId);
Refresh();
RefreshLayerManager();
return true;
}
void RenderLayerManager::ActivateDefaultLayer() {
if (!m_stage) return;
for (const auto& info : m_layers) {
if (info.isDefault) continue;
if (!m_stage->IsLayerMuted(info.layerIdentifier))
m_stage->MuteLayer(info.layerIdentifier);
}
if (!m_savedEditTargetIdBeforeMode.empty()) {
if (m_layerManager) {
m_layerManager->SetEditTarget(m_savedEditTargetIdBeforeMode);
} else if (SdfLayerHandle saved = SdfLayer::Find(m_savedEditTargetIdBeforeMode)) {
m_stage->SetEditTarget(UsdEditTarget(saved));
}
}
m_savedEditTargetIdBeforeMode.clear();
PersistActiveLayerId("");
Refresh();
RefreshLayerManager();
}
std::string RenderLayerManager::GetActiveRenderLayerId() const {
for (const auto& info : m_layers)
if (!info.isDefault && info.isActive)
return info.layerIdentifier;
return {};
}
void RenderLayerManager::PersistActiveLayerId(const std::string& layerId) {
if (!m_stage) return;
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return;
VtDictionary cld = rootLayer->GetCustomLayerData();
if (layerId.empty()) cld.erase(kActiveLayerKey);
else cld[kActiveLayerKey] = VtValue(layerId);
rootLayer->SetCustomLayerData(cld);
}
void RenderLayerManager::RestorePersistedActiveLayer() {
if (!m_stage) return;
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return;
VtDictionary cld = rootLayer->GetCustomLayerData();
auto it = cld.find(kActiveLayerKey);
if (it == cld.end() || !it->second.IsHolding<std::string>()) return;
std::string layerId = it->second.Get<std::string>();
if (FindRenderLayer(layerId))
ActivateRenderLayer(layerId);
}
// ---------------------------------------------------------------------------
// Membership (Sdf-level — works regardless of mute state)
// ---------------------------------------------------------------------------
std::vector<SdfPath> RenderLayerManager::GetMembers(const std::string& layerId) const {
std::vector<SdfPath> result;
SdfLayerHandle layer = FindRenderLayer(layerId);
if (!layer) return result;
SdfRelationshipSpecHandle rel = layer->GetRelationshipAtPath(MembersIncludesPath());
if (!rel) return result;
for (const SdfPath& p : rel->GetTargetPathList().GetAddedOrExplicitItems())
result.push_back(p);
return result;
}
bool RenderLayerManager::AddMember(const std::string& layerId, const SdfPath& path) {
SdfLayerHandle layer = FindRenderLayer(layerId);
if (!layer) return false;
SdfRelationshipSpecHandle rel = layer->GetRelationshipAtPath(MembersIncludesPath());
if (!rel) {
SdfPrimSpecHandle adminPrim = SdfCreatePrimInLayer(layer, kAdminPrimPath);
if (!adminPrim) return false;
rel = SdfRelationshipSpec::New(adminPrim, kMembersIncludesName.GetString(),
/*custom=*/false, SdfVariabilityUniform);
}
if (!rel) return false;
// Add() doesn't de-duplicate against existing explicit items — guard
// here so repeated adds of the same path (e.g. re-running a command, or
// a redundant drag) don't pile up duplicate targets.
auto existing = rel->GetTargetPathList().GetAddedOrExplicitItems();
if (std::find(existing.begin(), existing.end(), path) == existing.end())
rel->GetTargetPathList().Add(path);
if (GetActiveRenderLayerId() == layerId)
RecomputeIsolation(layer, layerId);
RefreshLayerManager(); // keeps StageEditorPanel's dirty-flag asterisk current
return true;
}
bool RenderLayerManager::RemoveMember(const std::string& layerId, const SdfPath& path) {
SdfLayerHandle layer = FindRenderLayer(layerId);
if (!layer) return false;
SdfRelationshipSpecHandle rel = layer->GetRelationshipAtPath(MembersIncludesPath());
if (!rel) return false;
rel->GetTargetPathList().Remove(path);
if (GetActiveRenderLayerId() == layerId)
RecomputeIsolation(layer, layerId);
RefreshLayerManager();
return true;
}
// ---------------------------------------------------------------------------
// Viewport isolation
// ---------------------------------------------------------------------------
void RenderLayerManager::ClearAutoHiddenPaths(const SdfLayerHandle& layer) {
if (!layer) return;
VtDictionary cld = layer->GetCustomLayerData();
auto it = cld.find(kAutoHiddenKey);
if (it == cld.end()) return;
if (it->second.IsHolding<VtArray<std::string>>()) {
for (const std::string& p : it->second.Get<VtArray<std::string>>()) {
SdfPath primPath(p);
SdfPrimSpecHandle owner = layer->GetPrimAtPath(primPath);
SdfAttributeSpecHandle attrSpec =
layer->GetAttributeAtPath(primPath.AppendProperty(TfToken("visibility")));
if (owner && attrSpec)
owner->RemoveProperty(attrSpec);
}
}
cld.erase(it);
layer->SetCustomLayerData(cld);
}
void RenderLayerManager::RecomputeIsolation(const SdfLayerHandle& layer, const std::string& layerId) {
if (!m_stage || !layer) return;
ClearAutoHiddenPaths(layer);
std::vector<SdfPath> memberList = GetMembers(layerId);
std::set<SdfPath> members(memberList.begin(), memberList.end());
// Every strict prefix of every member path: a subtree rooted at one of
// these paths might still contain a member somewhere below it, so it
// must be recursed into rather than hidden outright.
std::set<SdfPath> ancestors;
for (const SdfPath& m : memberList) {
SdfPath p = m.GetParentPath();
while (!p.IsEmpty()) {
if (!ancestors.insert(p).second) break; // already inserted by another member
if (p == SdfPath::AbsoluteRootPath()) break;
p = p.GetParentPath();
}
}
std::vector<std::string> newHidden;
std::function<void(const UsdPrim&)> visit = [&](const UsdPrim& prim) {
for (const UsdPrim& child : prim.GetChildren()) {
SdfPath cp = child.GetPath();
if (cp == kAdminPrimPath) continue;
if (members.count(cp)) {
// Exact member: whole subtree stays default-visible (expandPrims
// semantics — everything under a member is implicitly included),
// nothing to author, no need to recurse.
continue;
}
if (ancestors.count(cp)) {
visit(child); // a member lies somewhere below — keep exploring
continue;
}
UsdGeomImageable img(child);
if (img) {
UsdEditContext ec(m_stage, layer);
img.GetVisibilityAttr().Set(UsdGeomTokens->invisible);
newHidden.push_back(cp.GetString());
// Minimal cut: stop here, children inherit the hidden state.
} else {
// Can't author visibility on a non-imageable prim (e.g. a pure
// Scope holding unrelated data) — look deeper for an imageable
// descendant to hide instead, rather than silently failing to
// isolate this whole branch.
visit(child);
}
}
};
visit(m_stage->GetPseudoRoot());
if (!newHidden.empty()) {
VtDictionary cld = layer->GetCustomLayerData();
cld[kAutoHiddenKey] = VtValue(VtArray<std::string>(newHidden.begin(), newHidden.end()));
layer->SetCustomLayerData(cld);
}
}
} // namespace UsdLayerManager
+135
View File
@@ -0,0 +1,135 @@
#pragma once
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/layer.h>
#include <pxr/usd/sdf/path.h>
#include <string>
#include <vector>
namespace UsdLayerManager {
class LayerManager;
/// One row for the Render Layer panel. The "Default" entry (isDefault=true)
/// has no backing layer — it represents the unmodified base stage, i.e. every
/// render-layer sublayer muted.
struct RenderLayerInfo {
std::string name;
pxr::SdfLayerHandle layer; ///< empty for Default
std::string layerIdentifier; ///< empty for Default; the stable key used everywhere below
bool isActive = false;
bool isDefault = false;
};
/// Maya-Render-Layers-style layers built on the stage's own sublayer stack.
///
/// Each render layer is a dedicated, file-backed SdfLayer inserted as a
/// sublayer of the stage's root layer (tagged via SdfLayer custom layer data
/// so it's distinguishable from ordinary content sublayers). At most one is
/// ever unmuted ("active") at a time; activating one mutes the rest and
/// repoints the stage's ambient edit target at it, so every existing
/// attribute/transform/shader-connection/material-binding command in the app
/// already lands its edits in the right place with no changes of its own
/// (see docs/adr/0002-render-layer.md).
///
/// Membership (which lights/objects belong to a layer) is stored as a
/// UsdCollectionAPI-compatible relationship (`collection:members:includes`)
/// on a reserved, undefined ("over"-only, never rendered) admin prim inside
/// that layer's own SdfLayer. All reads/writes of it go through raw Sdf-level
/// API rather than UsdCollectionAPI/UsdStage, because muted layers are fully
/// excluded from stage composition — GetPrimAtPath would fail for every
/// layer except the currently-active one, and the panel needs to edit
/// membership on layers that aren't active.
///
/// Switching layers also isolates the viewport to that layer's members: a
/// single top-down traversal authors the minimal set of
/// `visibility = invisible` opinions (into the active layer's own SdfLayer)
/// needed to hide every subtree that contains no member, tracked via that
/// layer's custom layer data so it can be precisely cleared and recomputed
/// without disturbing deliberate visibility overrides.
class RenderLayerManager {
public:
RenderLayerManager();
~RenderLayerManager();
void SetStage(pxr::UsdStageRefPtr stage);
void SetLayerManager(LayerManager* mgr) { m_layerManager = mgr; }
void Refresh();
/// Default first, then user-created render layers in sublayer order.
std::vector<RenderLayerInfo> GetRenderLayers() const { return m_layers; }
/// Creates a new file-backed render layer next to the stage's root layer
/// (under a ".renderlayers" subdirectory), muted, with an empty member
/// list. Returns the new layer's identifier, or empty on failure.
std::string CreateRenderLayer(const std::string& name);
bool DeleteRenderLayer(const std::string& layerId);
/// Re-inserts a render layer previously removed by DeleteRenderLayer — its
/// backing file is left on disk untouched by DeleteRenderLayer, so this
/// just restores the sublayer reference (always at the top of the stack,
/// same as a newly-created layer). Used by RenderLayerDeleteCommand::Undo().
bool RestoreDeletedRenderLayer(const std::string& layerIdentifier);
bool RenameRenderLayer(const std::string& layerId, const std::string& newName);
/// Mutes every other render-layer sublayer, unmutes layerId, repoints the
/// stage's ambient edit target at it, and recomputes viewport isolation.
/// The first time this is called while Default is active, it remembers
/// the current edit target so ActivateDefaultLayer() can restore it later
/// — callers (e.g. an undoable switch command) don't need to manage that
/// themselves; it's tracked here so it stays correct across Undo/Redo.
bool ActivateRenderLayer(const std::string& layerId);
/// Mutes every render-layer sublayer (back to the unmodified base stage)
/// and restores whatever the edit target was before render-layer mode
/// was first entered (see ActivateRenderLayer's note).
void ActivateDefaultLayer();
std::string GetActiveRenderLayerId() const;
bool IsDefaultActive() const { return GetActiveRenderLayerId().empty(); }
/// Sdf-level: the raw, unexpanded `includes` target list. Works
/// regardless of whether layerId is currently muted.
std::vector<pxr::SdfPath> GetMembers(const std::string& layerId) const;
bool AddMember(const std::string& layerId, const pxr::SdfPath& path);
bool RemoveMember(const std::string& layerId, const pxr::SdfPath& path);
/// Re-applies whichever render layer was active the last time this stage
/// was saved (persisted as root-layer custom layer data, since USD mute
/// state itself is a runtime flag and isn't serialized). Call once after
/// SetStage(). No-op if nothing was persisted, or the persisted layer no
/// longer exists.
void RestorePersistedActiveLayer();
/// True if this layer carries the "this sublayer is a render layer"
/// marker RenderLayerManager itself authors on creation. Exposed so
/// other panels (e.g. StageEditorPanel) that walk the raw sublayer list
/// can recognize and guard-rail render-layer sublayers without
/// duplicating the custom-layer-data key.
static bool IsRenderLayerSublayer(const pxr::SdfLayerHandle& layer);
private:
void BuildList();
// LayerManager keeps its own cached sublayer/mute-state snapshot,
// rebuilt only on its own SetStage()/Refresh()/mutator calls — it has no
// way to know when RenderLayerManager mutates the root layer's sublayer
// stack or a layer's mute state directly (bypassing LayerManager's own
// methods, which every RenderLayerManager mutator below does). Called
// after every such mutation so StageEditorPanel (backed by LayerManager)
// doesn't show a stale sublayer list.
void RefreshLayerManager();
pxr::SdfLayerHandle FindRenderLayer(const std::string& layerId) const;
void RecomputeIsolation(const pxr::SdfLayerHandle& layer, const std::string& layerId);
void ClearAutoHiddenPaths(const pxr::SdfLayerHandle& layer);
void PersistActiveLayerId(const std::string& layerId);
pxr::UsdStageRefPtr m_stage;
LayerManager* m_layerManager = nullptr;
std::vector<RenderLayerInfo> m_layers; // [0] is always the Default entry
// Strong refs so muted render-layer SdfLayers stay alive between
// Refresh() calls (mirrors LayerManager::m_layerRefs).
std::vector<pxr::SdfLayerRefPtr> m_layerRefs;
// Edit target identifier captured the moment render-layer mode is first
// entered (Default -> any render layer); restored and cleared by
// ActivateDefaultLayer(). Empty when not currently "in mode".
std::string m_savedEditTargetIdBeforeMode;
};
} // namespace UsdLayerManager
+1 -1
View File
@@ -174,7 +174,7 @@ UsdSceneRenderer::UsdSceneRenderer()
, m_aaEnabled(false) , m_aaEnabled(false)
, m_backgroundColor(0.15f, 0.15f, 0.15f) , m_backgroundColor(0.15f, 0.15f, 0.15f)
, m_shadingMode(ShadingMode::SmoothShaded) , m_shadingMode(ShadingMode::SmoothShaded)
, m_cullStyle(CullStyle::BackUnlessDoubleSided) , m_cullStyle(CullStyle::Nothing) // matches usdview; see ViewportTileSettings::cullStyle
, m_colorCorrectionMode(ColorCorrectionMode::sRGB) , m_colorCorrectionMode(ColorCorrectionMode::sRGB)
, m_ambientLightOnly(true) , m_ambientLightOnly(true)
, m_domeLightEnabled(false) , m_domeLightEnabled(false)
@@ -1,5 +1,6 @@
#include "ConnectShaderAttrsCommand.h" #include "ConnectShaderAttrsCommand.h"
#include "../../utils/Logger.h" #include "../../utils/Logger.h"
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/usdShade/shader.h> #include <pxr/usd/usdShade/shader.h>
#include <pxr/usd/usdShade/connectableAPI.h> #include <pxr/usd/usdShade/connectableAPI.h>
#include <pxr/base/tf/token.h> #include <pxr/base/tf/token.h>
@@ -12,7 +13,8 @@ ConnectShaderAttrsCommand::ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfValueTypeName& destType, const pxr::SdfValueTypeName& destType,
const pxr::SdfPath& sourceNode, const pxr::SdfPath& sourceNode,
const std::string& sourceOutput, const std::string& sourceOutput,
const pxr::SdfValueTypeName& sourceType) const pxr::SdfValueTypeName& sourceType,
pxr::SdfLayerHandle editLayer)
: m_stage(stage) : m_stage(stage)
, m_destNode(destNode) , m_destNode(destNode)
, m_destInput(destInput) , m_destInput(destInput)
@@ -20,6 +22,7 @@ ConnectShaderAttrsCommand::ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
, m_sourceNode(sourceNode) , m_sourceNode(sourceNode)
, m_sourceOutput(sourceOutput) , m_sourceOutput(sourceOutput)
, m_sourceType(sourceType) , m_sourceType(sourceType)
, m_editLayer(editLayer)
, m_description("Connect " + sourceNode.GetName() + "." + sourceOutput + , m_description("Connect " + sourceNode.GetName() + "." + sourceOutput +
" -> " + destNode.GetName() + "." + destInput) " -> " + destNode.GetName() + "." + destInput)
{ {
@@ -50,9 +53,16 @@ void ConnectShaderAttrsCommand::Execute() {
return; return;
} }
if (m_editLayer) {
pxr::UsdEditContext ec(m_stage, m_editLayer);
pxr::UsdShadeInput destInput = destShader.CreateInput(pxr::TfToken(m_destInput), m_destType); pxr::UsdShadeInput destInput = destShader.CreateInput(pxr::TfToken(m_destInput), m_destType);
pxr::UsdShadeOutput sourceOutput = sourceShader.CreateOutput(pxr::TfToken(m_sourceOutput), m_sourceType); pxr::UsdShadeOutput sourceOutput = sourceShader.CreateOutput(pxr::TfToken(m_sourceOutput), m_sourceType);
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, sourceOutput); pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, sourceOutput);
} else {
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) { } catch (const std::exception& e) {
LOG_ERROR(std::string("ConnectShaderAttrsCommand::Execute error: ") + e.what()); LOG_ERROR(std::string("ConnectShaderAttrsCommand::Execute error: ") + e.what());
} }
@@ -66,6 +76,7 @@ void ConnectShaderAttrsCommand::Undo() {
pxr::UsdShadeInput destInput = destShader.GetInput(pxr::TfToken(m_destInput)); pxr::UsdShadeInput destInput = destShader.GetInput(pxr::TfToken(m_destInput));
if (!destInput) return; if (!destInput) return;
auto doUndo = [&]() {
if (m_hadPriorConnection) { if (m_hadPriorConnection) {
pxr::UsdShadeShader priorSourceShader(m_stage->GetPrimAtPath(m_priorSourceNode)); pxr::UsdShadeShader priorSourceShader(m_stage->GetPrimAtPath(m_priorSourceNode));
pxr::UsdShadeOutput priorSourceOutput = priorSourceShader.GetOutput(pxr::TfToken(m_priorSourceOutput)); pxr::UsdShadeOutput priorSourceOutput = priorSourceShader.GetOutput(pxr::TfToken(m_priorSourceOutput));
@@ -74,6 +85,14 @@ void ConnectShaderAttrsCommand::Undo() {
} else { } else {
pxr::UsdShadeConnectableAPI::DisconnectSource(destInput); pxr::UsdShadeConnectableAPI::DisconnectSource(destInput);
} }
};
if (m_editLayer) {
pxr::UsdEditContext ec(m_stage, m_editLayer);
doUndo();
} else {
doUndo();
}
} catch (const std::exception& e) { } catch (const std::exception& e) {
LOG_ERROR(std::string("ConnectShaderAttrsCommand::Undo error: ") + e.what()); LOG_ERROR(std::string("ConnectShaderAttrsCommand::Undo error: ") + e.what());
} }
@@ -4,6 +4,7 @@
#include <pxr/usd/usd/stage.h> #include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h> #include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/valueTypeName.h> #include <pxr/usd/sdf/valueTypeName.h>
#include <pxr/usd/sdf/layer.h>
#include <string> #include <string>
namespace UsdLayerManager { namespace UsdLayerManager {
@@ -14,13 +15,19 @@ namespace UsdLayerManager {
/// Undo can restore it exactly rather than merely disconnecting. /// Undo can restore it exactly rather than merely disconnecting.
class ConnectShaderAttrsCommand : public ICommand { class ConnectShaderAttrsCommand : public ICommand {
public: public:
/// editLayer: layer to author into (mirrors TransformCommand's pattern).
/// Captured at construction time so Undo() targets the same layer as
/// Execute() even if the stage's ambient edit target changes in between
/// (e.g. a render-layer switch) — pass null/empty to fall back to
/// whatever the ambient edit target is at call time (today's behavior).
ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage, ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& destNode, const pxr::SdfPath& destNode,
const std::string& destInput, const std::string& destInput,
const pxr::SdfValueTypeName& destType, const pxr::SdfValueTypeName& destType,
const pxr::SdfPath& sourceNode, const pxr::SdfPath& sourceNode,
const std::string& sourceOutput, const std::string& sourceOutput,
const pxr::SdfValueTypeName& sourceType); const pxr::SdfValueTypeName& sourceType,
pxr::SdfLayerHandle editLayer = pxr::SdfLayerHandle());
void Execute() override; void Execute() override;
void Undo() override; void Undo() override;
@@ -34,6 +41,7 @@ private:
pxr::SdfPath m_sourceNode; pxr::SdfPath m_sourceNode;
std::string m_sourceOutput; std::string m_sourceOutput;
pxr::SdfValueTypeName m_sourceType; pxr::SdfValueTypeName m_sourceType;
pxr::SdfLayerHandle m_editLayer;
std::string m_description; std::string m_description;
bool m_hadPriorConnection = false; bool m_hadPriorConnection = false;
@@ -1,5 +1,6 @@
#include "DisconnectShaderAttrCommand.h" #include "DisconnectShaderAttrCommand.h"
#include "../../utils/Logger.h" #include "../../utils/Logger.h"
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/usdShade/shader.h> #include <pxr/usd/usdShade/shader.h>
#include <pxr/usd/usdShade/connectableAPI.h> #include <pxr/usd/usdShade/connectableAPI.h>
#include <pxr/base/tf/token.h> #include <pxr/base/tf/token.h>
@@ -8,10 +9,12 @@ namespace UsdLayerManager {
DisconnectShaderAttrCommand::DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage, DisconnectShaderAttrCommand::DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& destNode, const pxr::SdfPath& destNode,
const std::string& destInput) const std::string& destInput,
pxr::SdfLayerHandle editLayer)
: m_stage(stage) : m_stage(stage)
, m_destNode(destNode) , m_destNode(destNode)
, m_destInput(destInput) , m_destInput(destInput)
, m_editLayer(editLayer)
, m_description("Disconnect " + destNode.GetName() + "." + destInput) , m_description("Disconnect " + destNode.GetName() + "." + destInput)
{ {
if (!stage) return; if (!stage) return;
@@ -38,7 +41,13 @@ void DisconnectShaderAttrCommand::Execute() {
if (!destShader) return; if (!destShader) return;
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput)); pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput));
if (!input) return; if (!input) return;
if (m_editLayer) {
pxr::UsdEditContext ec(m_stage, m_editLayer);
pxr::UsdShadeConnectableAPI::DisconnectSource(input); pxr::UsdShadeConnectableAPI::DisconnectSource(input);
} else {
pxr::UsdShadeConnectableAPI::DisconnectSource(input);
}
} catch (const std::exception& e) { } catch (const std::exception& e) {
LOG_ERROR(std::string("DisconnectShaderAttrCommand::Execute error: ") + e.what()); LOG_ERROR(std::string("DisconnectShaderAttrCommand::Execute error: ") + e.what());
} }
@@ -53,8 +62,14 @@ void DisconnectShaderAttrCommand::Undo() {
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput)); pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput));
pxr::UsdShadeOutput output = sourceShader.GetOutput(pxr::TfToken(m_sourceOutput)); pxr::UsdShadeOutput output = sourceShader.GetOutput(pxr::TfToken(m_sourceOutput));
if (input && output) if (!input || !output) return;
if (m_editLayer) {
pxr::UsdEditContext ec(m_stage, m_editLayer);
pxr::UsdShadeConnectableAPI::ConnectToSource(input, output); pxr::UsdShadeConnectableAPI::ConnectToSource(input, output);
} else {
pxr::UsdShadeConnectableAPI::ConnectToSource(input, output);
}
} catch (const std::exception& e) { } catch (const std::exception& e) {
LOG_ERROR(std::string("DisconnectShaderAttrCommand::Undo error: ") + e.what()); LOG_ERROR(std::string("DisconnectShaderAttrCommand::Undo error: ") + e.what());
} }
@@ -3,6 +3,7 @@
#include "../CommandHistory.h" #include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h> #include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h> #include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/layer.h>
#include <string> #include <string>
namespace UsdLayerManager { namespace UsdLayerManager {
@@ -11,9 +12,13 @@ namespace UsdLayerManager {
/// source at construction time so Undo can restore it. /// source at construction time so Undo can restore it.
class DisconnectShaderAttrCommand : public ICommand { class DisconnectShaderAttrCommand : public ICommand {
public: public:
/// editLayer: see ConnectShaderAttrsCommand's constructor doc — same
/// capture-at-construction-time pattern, null/empty falls back to the
/// ambient edit target at call time (today's behavior).
DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage, DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& destNode, const pxr::SdfPath& destNode,
const std::string& destInput); const std::string& destInput,
pxr::SdfLayerHandle editLayer = pxr::SdfLayerHandle());
void Execute() override; void Execute() override;
void Undo() override; void Undo() override;
@@ -23,6 +28,7 @@ private:
pxr::UsdStageRefPtr m_stage; pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_destNode; pxr::SdfPath m_destNode;
std::string m_destInput; std::string m_destInput;
pxr::SdfLayerHandle m_editLayer;
std::string m_description; std::string m_description;
bool m_hadConnection = false; bool m_hadConnection = false;
+102
View File
@@ -0,0 +1,102 @@
#include "RenderLayerCommands.h"
namespace UsdLayerManager {
// ─── RenderLayerCreateCommand ─────────────────────────────────────────────
RenderLayerCreateCommand::RenderLayerCreateCommand(RenderLayerManager* mgr, std::string name)
: m_mgr(mgr)
, m_name(std::move(name))
, m_description("Create Render Layer " + m_name)
{
}
void RenderLayerCreateCommand::Execute() {
if (!m_mgr) return;
m_layerId = m_mgr->CreateRenderLayer(m_name);
}
void RenderLayerCreateCommand::Undo() {
if (!m_mgr || m_layerId.empty()) return;
m_mgr->DeleteRenderLayer(m_layerId);
}
// ─── RenderLayerDeleteCommand ─────────────────────────────────────────────
RenderLayerDeleteCommand::RenderLayerDeleteCommand(RenderLayerManager* mgr, std::string layerId)
: m_mgr(mgr)
, m_layerId(std::move(layerId))
, m_description("Delete Render Layer")
{
if (!mgr) return;
for (const auto& info : mgr->GetRenderLayers()) {
if (!info.isDefault && info.layerIdentifier == m_layerId) {
m_description = "Delete Render Layer " + info.name;
break;
}
}
}
void RenderLayerDeleteCommand::Execute() {
if (!m_mgr) return;
m_mgr->DeleteRenderLayer(m_layerId);
}
void RenderLayerDeleteCommand::Undo() {
if (!m_mgr || m_layerId.empty()) return;
m_mgr->RestoreDeletedRenderLayer(m_layerId);
}
// ─── RenderLayerRenameCommand ─────────────────────────────────────────────
RenderLayerRenameCommand::RenderLayerRenameCommand(RenderLayerManager* mgr, std::string layerId,
std::string newName)
: m_mgr(mgr)
, m_layerId(std::move(layerId))
, m_newName(std::move(newName))
, m_description("Rename Render Layer")
{
if (!mgr) return;
for (const auto& info : mgr->GetRenderLayers()) {
if (!info.isDefault && info.layerIdentifier == m_layerId) {
m_oldName = info.name;
m_description = "Rename Render Layer " + info.name + " -> " + m_newName;
break;
}
}
}
void RenderLayerRenameCommand::Execute() {
if (!m_mgr) return;
m_mgr->RenameRenderLayer(m_layerId, m_newName);
}
void RenderLayerRenameCommand::Undo() {
if (!m_mgr || m_oldName.empty()) return;
m_mgr->RenameRenderLayer(m_layerId, m_oldName);
}
// ─── RenderLayerActivateCommand ───────────────────────────────────────────
RenderLayerActivateCommand::RenderLayerActivateCommand(RenderLayerManager* mgr, std::string targetLayerId)
: m_mgr(mgr)
, m_targetLayerId(std::move(targetLayerId))
, m_description(m_targetLayerId.empty() ? "Activate Render Layer: Default"
: "Activate Render Layer")
{
if (mgr) m_priorLayerId = mgr->GetActiveRenderLayerId();
}
void RenderLayerActivateCommand::Execute() {
if (!m_mgr) return;
if (m_targetLayerId.empty()) m_mgr->ActivateDefaultLayer();
else m_mgr->ActivateRenderLayer(m_targetLayerId);
}
void RenderLayerActivateCommand::Undo() {
if (!m_mgr) return;
if (m_priorLayerId.empty()) m_mgr->ActivateDefaultLayer();
else m_mgr->ActivateRenderLayer(m_priorLayerId);
}
} // namespace UsdLayerManager
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include "../CommandHistory.h"
#include "../RenderLayerManager.h"
#include <string>
namespace UsdLayerManager {
/// Undo removes the created layer via RenderLayerManager::DeleteRenderLayer.
class RenderLayerCreateCommand : public ICommand {
public:
RenderLayerCreateCommand(RenderLayerManager* mgr, std::string name);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
/// Valid after Execute() — the new layer's identifier, so the panel can
/// select it immediately.
const std::string& GetCreatedLayerId() const { return m_layerId; }
private:
RenderLayerManager* m_mgr;
std::string m_name;
std::string m_layerId;
std::string m_description;
};
/// The backing file is left on disk by RenderLayerManager::DeleteRenderLayer
/// (only the sublayer reference is removed), so Undo just re-inserts it.
class RenderLayerDeleteCommand : public ICommand {
public:
RenderLayerDeleteCommand(RenderLayerManager* mgr, std::string layerId);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
RenderLayerManager* m_mgr;
std::string m_layerId;
std::string m_description;
};
class RenderLayerRenameCommand : public ICommand {
public:
RenderLayerRenameCommand(RenderLayerManager* mgr, std::string layerId, std::string newName);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
RenderLayerManager* m_mgr;
std::string m_layerId;
std::string m_newName;
std::string m_oldName;
std::string m_description;
};
/// Switches the active render layer (or back to Default). RenderLayerManager
/// itself tracks the edit target to restore on returning to Default, so this
/// command only needs to remember which layer was active before the switch.
class RenderLayerActivateCommand : public ICommand {
public:
/// targetLayerId empty = activate Default.
RenderLayerActivateCommand(RenderLayerManager* mgr, std::string targetLayerId);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
RenderLayerManager* m_mgr;
std::string m_targetLayerId; // "" = Default
std::string m_priorLayerId; // captured at construction; "" = Default
std::string m_description;
};
} // namespace UsdLayerManager
@@ -0,0 +1,57 @@
#include "RenderLayerMembershipCommand.h"
namespace UsdLayerManager {
// ─── RenderLayerAddMembersCommand ─────────────────────────────────────────
RenderLayerAddMembersCommand::RenderLayerAddMembersCommand(RenderLayerManager* mgr,
std::string layerId,
std::vector<pxr::SdfPath> paths)
: m_mgr(mgr)
, m_layerId(std::move(layerId))
, m_paths(std::move(paths))
, m_description(m_paths.size() == 1
? ("Add " + m_paths.front().GetString() + " to Render Layer")
: ("Add " + std::to_string(m_paths.size()) + " Members to Render Layer"))
{
}
void RenderLayerAddMembersCommand::Execute() {
if (!m_mgr) return;
for (const auto& p : m_paths)
m_mgr->AddMember(m_layerId, p);
}
void RenderLayerAddMembersCommand::Undo() {
if (!m_mgr) return;
for (const auto& p : m_paths)
m_mgr->RemoveMember(m_layerId, p);
}
// ─── RenderLayerRemoveMembersCommand ──────────────────────────────────────
RenderLayerRemoveMembersCommand::RenderLayerRemoveMembersCommand(RenderLayerManager* mgr,
std::string layerId,
std::vector<pxr::SdfPath> paths)
: m_mgr(mgr)
, m_layerId(std::move(layerId))
, m_paths(std::move(paths))
, m_description(m_paths.size() == 1
? ("Remove " + m_paths.front().GetString() + " from Render Layer")
: ("Remove " + std::to_string(m_paths.size()) + " Members from Render Layer"))
{
}
void RenderLayerRemoveMembersCommand::Execute() {
if (!m_mgr) return;
for (const auto& p : m_paths)
m_mgr->RemoveMember(m_layerId, p);
}
void RenderLayerRemoveMembersCommand::Undo() {
if (!m_mgr) return;
for (const auto& p : m_paths)
m_mgr->AddMember(m_layerId, p);
}
} // namespace UsdLayerManager
@@ -0,0 +1,44 @@
#pragma once
#include "../CommandHistory.h"
#include "../RenderLayerManager.h"
#include <pxr/usd/sdf/path.h>
#include <string>
#include <vector>
namespace UsdLayerManager {
/// Adds one or more prim paths to a render layer's membership (Sdf-level, so
/// this works whether or not layerId is currently active). Re-runs viewport
/// isolation if layerId happens to be the active layer.
class RenderLayerAddMembersCommand : public ICommand {
public:
RenderLayerAddMembersCommand(RenderLayerManager* mgr, std::string layerId,
std::vector<pxr::SdfPath> paths);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
RenderLayerManager* m_mgr;
std::string m_layerId;
std::vector<pxr::SdfPath> m_paths;
std::string m_description;
};
class RenderLayerRemoveMembersCommand : public ICommand {
public:
RenderLayerRemoveMembersCommand(RenderLayerManager* mgr, std::string layerId,
std::vector<pxr::SdfPath> paths);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
RenderLayerManager* m_mgr;
std::string m_layerId;
std::vector<pxr::SdfPath> m_paths;
std::string m_description;
};
} // namespace UsdLayerManager
+39
View File
@@ -19,6 +19,45 @@ static void SetUsdPluginPath() {
std::string pluginPath = exeDir + "\\usd"; std::string pluginPath = exeDir + "\\usd";
SetEnvironmentVariableA("PXR_PLUGINPATH_NAME", pluginPath.c_str()); SetEnvironmentVariableA("PXR_PLUGINPATH_NAME", pluginPath.c_str());
// --- MaterialX / Arnold OSL include path -------------------------------
// hdArnold compiles MaterialX shaders by generating OSL whose first line is
// #include "mx_funcs.h"
// and compiling it as an "osl" node's `code` parameter. Without an include
// path that compile fails with:
// [osl] error: <buffer>:2:10: fatal error: 'mx_funcs.h' file not found
//
// The knob is Arnold's options.osl_includepath, which hdArnold exposes as the
// HDARNOLD_osl_includepath env setting (render_delegate/config.cpp).
//
// THIS MUST BE SET BEFORE ANY PLUGIN DLL IS LOADED. TF_DEFINE_ENV_SETTING
// registers a TF_REGISTRY_FUNCTION that eagerly calls TfGetEnvSetting() and
// caches the value permanently when hdArnold.dll loads -- which the
// LoadLibraryA pre-flight below does. Setting it later (e.g. from
// Application::Initialize) is silently ignored.
//
// Set via both the CRT (_putenv_s -> getenv) and the Win32 block
// (SetEnvironmentVariableA) since the two are not kept in sync on Windows.
{
namespace fs = std::filesystem;
const std::string mtlxLibs = exeDir + "\\libraries";
const std::string mtlxOslInclude = mtlxLibs + "\\stdlib\\genosl\\include";
if (fs::exists(mtlxOslInclude)) {
if (!getenv("HDARNOLD_osl_includepath")) {
_putenv_s("HDARNOLD_osl_includepath", mtlxOslInclude.c_str());
SetEnvironmentVariableA("HDARNOLD_osl_includepath",
mtlxOslInclude.c_str());
}
// Nodedef lookup (MATERIALX_NODE_DEFINITIONS). Separate knob from
// the OSL include path above -- it does NOT fix the include error.
if (!getenv("PXR_MTLX_STDLIB_SEARCH_PATHS")) {
_putenv_s("PXR_MTLX_STDLIB_SEARCH_PATHS", mtlxLibs.c_str());
SetEnvironmentVariableA("PXR_MTLX_STDLIB_SEARCH_PATHS",
mtlxLibs.c_str());
}
}
}
std::vector<std::string> pluginPaths; std::vector<std::string> pluginPaths;
WIN32_FIND_DATAA findData; WIN32_FIND_DATAA findData;
std::string searchPattern = pluginPath + "\\*"; std::string searchPattern = pluginPath + "\\*";
+151 -1
View File
@@ -41,6 +41,7 @@ Application::Application()
: m_showDemoWindow(false) : m_showDemoWindow(false)
, m_showStageInfo(true) , m_showStageInfo(true)
, m_showStageEditor(true) , m_showStageEditor(true)
, m_showRenderLayerPanel(true)
, m_showSceneHierarchy(true) , m_showSceneHierarchy(true)
, m_showViewport(true) , m_showViewport(true)
, m_showPropertyPanel(true) , m_showPropertyPanel(true)
@@ -67,15 +68,23 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
// Create managers // Create managers
m_stageManager = std::make_unique<UsdStageManager>(); m_stageManager = std::make_unique<UsdStageManager>();
m_layerManager = std::make_unique<LayerManager>(); m_layerManager = std::make_unique<LayerManager>();
m_renderLayerManager = std::make_unique<RenderLayerManager>();
m_renderLayerManager->SetLayerManager(m_layerManager.get());
m_propertyManager = std::make_unique<PropertyManager>(); m_propertyManager = std::make_unique<PropertyManager>();
m_propertyManager->SetCommandHistory(&m_commandHistory); m_propertyManager->SetCommandHistory(&m_commandHistory);
m_stageEditorPanel = std::make_unique<StageEditorPanel>(); m_stageEditorPanel = std::make_unique<StageEditorPanel>();
m_stageEditorPanel->SetLayerManager(m_layerManager.get()); m_stageEditorPanel->SetLayerManager(m_layerManager.get());
m_stageEditorPanel->SetCommandHistory(&m_commandHistory); m_stageEditorPanel->SetCommandHistory(&m_commandHistory);
m_stageEditorPanel->SetRenderLayerManager(m_renderLayerManager.get());
m_sceneHierarchyPanel = std::make_unique<SceneHierarchyPanel>(); m_sceneHierarchyPanel = std::make_unique<SceneHierarchyPanel>();
m_sceneHierarchyPanel->SetPropertyManager(m_propertyManager.get()); m_sceneHierarchyPanel->SetPropertyManager(m_propertyManager.get());
m_sceneHierarchyPanel->SetCommandHistory(&m_commandHistory); m_sceneHierarchyPanel->SetCommandHistory(&m_commandHistory);
m_sceneHierarchyPanel->SetLayerManager(m_layerManager.get()); m_sceneHierarchyPanel->SetLayerManager(m_layerManager.get());
m_sceneHierarchyPanel->SetRenderLayerManager(m_renderLayerManager.get());
m_renderLayerPanel = std::make_unique<RenderLayerPanel>();
m_renderLayerPanel->SetRenderLayerManager(m_renderLayerManager.get());
m_renderLayerPanel->SetCommandHistory(&m_commandHistory);
m_renderLayerPanel->SetSceneHierarchyPanel(m_sceneHierarchyPanel.get());
m_viewportPanel = std::make_unique<ViewportPanel>(); m_viewportPanel = std::make_unique<ViewportPanel>();
m_viewportPanel->SetCommandHistory(&m_commandHistory); m_viewportPanel->SetCommandHistory(&m_commandHistory);
m_propertyPanel = std::make_unique<PropertyPanel>(); m_propertyPanel = std::make_unique<PropertyPanel>();
@@ -116,6 +125,7 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
m_timelinePanel->SetIconManager(m_iconManager.get()); m_timelinePanel->SetIconManager(m_iconManager.get());
m_stageEditorPanel->SetIconManager(m_iconManager.get()); m_stageEditorPanel->SetIconManager(m_iconManager.get());
m_materialEditorPanel->SetIconManager(m_iconManager.get()); m_materialEditorPanel->SetIconManager(m_iconManager.get());
m_renderLayerPanel->SetIconManager(m_iconManager.get());
m_sceneHierarchyPanel->SetOnPrimSelected( m_sceneHierarchyPanel->SetOnPrimSelected(
[this](const std::string& path) { [this](const std::string& path) {
@@ -177,6 +187,18 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
} }
} }
// NOTE: HDARNOLD_osl_includepath / PXR_MTLX_STDLIB_SEARCH_PATHS are set in
// main.cpp, NOT here. They must be in the environment before the plugin
// DLLs are loaded, because TF_DEFINE_ENV_SETTING caches the value when
// hdArnold.dll registers its settings. Setting them at this point is too
// late and is silently ignored.
if (const char* oslInc = getenv("HDARNOLD_osl_includepath")) {
LOG_INFO(std::string("Arnold OSL include path: ") + oslInc);
} else {
LOG_WARNING("HDARNOLD_osl_includepath not set — Arnold MaterialX "
"shaders will fail to compile ('mx_funcs.h' not found)");
}
// Load per-viewport settings and global preferences from AppData. // Load per-viewport settings and global preferences from AppData.
if (const char* appData = getenv("APPDATA")) { if (const char* appData = getenv("APPDATA")) {
namespace fs = std::filesystem; namespace fs = std::filesystem;
@@ -186,6 +208,7 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
m_prefsPath = (dir / "preferences.ini").string(); m_prefsPath = (dir / "preferences.ini").string();
m_viewportPanel->LoadSettings(m_viewportSettingsPath); m_viewportPanel->LoadSettings(m_viewportSettingsPath);
LoadPreferences(); LoadPreferences();
ValidateOcioPreferences();
// Apply pref delegate only to tiles that have no saved delegate. // Apply pref delegate only to tiles that have no saved delegate.
m_viewportPanel->ApplyDefaultDelegate(m_prefs.renderDelegate); m_viewportPanel->ApplyDefaultDelegate(m_prefs.renderDelegate);
// Color correction is always global — apply to all tiles. // Color correction is always global — apply to all tiles.
@@ -223,10 +246,12 @@ void Application::Shutdown() {
m_sceneHierarchyPanel.reset(); m_sceneHierarchyPanel.reset();
m_propertyPanel.reset(); m_propertyPanel.reset();
m_stageEditorPanel.reset(); m_stageEditorPanel.reset();
m_renderLayerPanel.reset();
// Owns the shader-ball preview's Hydra engine + GL draw target — must be // Owns the shader-ball preview's Hydra engine + GL draw target — must be
// destroyed while the GL context still exists, like m_viewportPanel. // destroyed while the GL context still exists, like m_viewportPanel.
m_materialEditorPanel.reset(); m_materialEditorPanel.reset();
m_propertyManager.reset(); m_propertyManager.reset();
m_renderLayerManager.reset();
m_layerManager.reset(); m_layerManager.reset();
if (m_iconManager) { if (m_iconManager) {
@@ -251,8 +276,12 @@ void Application::RefreshManagers() {
if (m_stageManager->HasStage()) { if (m_stageManager->HasStage()) {
auto stage = m_stageManager->GetStage(); auto stage = m_stageManager->GetStage();
m_layerManager->SetStage(stage); m_layerManager->SetStage(stage);
// After LayerManager (ActivateRenderLayer routes edit-target changes
// through it) but before RestorePersistedActiveLayer needs it.
m_renderLayerManager->SetStage(stage);
m_propertyManager->SetStage(stage); m_propertyManager->SetStage(stage);
m_sceneHierarchyPanel->SetStage(stage); m_sceneHierarchyPanel->SetStage(stage);
m_renderLayerPanel->SetStage(stage);
m_viewportPanel->SetStage(stage); m_viewportPanel->SetStage(stage);
m_viewportPanel->FrameScene(); m_viewportPanel->FrameScene();
m_propertyPanel->SetStage(stage); m_propertyPanel->SetStage(stage);
@@ -260,10 +289,16 @@ void Application::RefreshManagers() {
m_curveEditorPanel->SetStage(stage); m_curveEditorPanel->SetStage(stage);
m_materialManager->SetStage(stage); m_materialManager->SetStage(stage);
m_materialEditorPanel->SetStage(stage); m_materialEditorPanel->SetStage(stage);
// Re-applies whichever render layer was active last time this stage
// was saved (mute state itself isn't serialized — see
// RenderLayerManager::RestorePersistedActiveLayer).
m_renderLayerManager->RestorePersistedActiveLayer();
} else { } else {
m_layerManager->SetStage(nullptr); m_layerManager->SetStage(nullptr);
m_renderLayerManager->SetStage(nullptr);
m_propertyManager->SetStage(nullptr); m_propertyManager->SetStage(nullptr);
m_sceneHierarchyPanel->SetStage(nullptr); m_sceneHierarchyPanel->SetStage(nullptr);
m_renderLayerPanel->SetStage(nullptr);
m_viewportPanel->SetStage(nullptr); m_viewportPanel->SetStage(nullptr);
m_propertyPanel->SetStage(nullptr); m_propertyPanel->SetStage(nullptr);
m_timelinePanel->SetStage(nullptr); m_timelinePanel->SetStage(nullptr);
@@ -315,6 +350,12 @@ void Application::RenderUI() {
ImGui::End(); ImGui::End();
} }
if (m_showRenderLayerPanel) {
ImGui::Begin("Render Layers", &m_showRenderLayerPanel, ImGuiWindowFlags_NoCollapse);
m_renderLayerPanel->Render();
ImGui::End();
}
if (m_showViewport) { if (m_showViewport) {
m_viewportPanel->Render(&m_showViewport); m_viewportPanel->Render(&m_showViewport);
} }
@@ -429,11 +470,85 @@ void Application::LoadPreferences()
m_prefs.materialBrowserWidth = [&]{ try { return std::stof(val); } catch(...){ return 220.0f; } }(); m_prefs.materialBrowserWidth = [&]{ try { return std::stof(val); } catch(...){ return 220.0f; } }();
else if (key == "MaterialPreviewWidth") else if (key == "MaterialPreviewWidth")
m_prefs.materialPreviewWidth = [&]{ try { return std::stof(val); } catch(...){ return 320.0f; } }(); m_prefs.materialPreviewWidth = [&]{ try { return std::stof(val); } catch(...){ return 320.0f; } }();
else if (key == "KeepGraphNodeViewSettingsInUsd")
m_prefs.keepGraphNodeViewSettingsInUsd = (val == "1");
} }
if (m_materialEditorPanel) if (m_materialEditorPanel) {
m_materialEditorPanel->SetColumnWidths(m_prefs.materialBrowserWidth, m_materialEditorPanel->SetColumnWidths(m_prefs.materialBrowserWidth,
m_prefs.materialPreviewWidth); m_prefs.materialPreviewWidth);
m_materialEditorPanel->SetKeepGraphNodeViewSettingsInUsd(m_prefs.keepGraphNodeViewSettingsInUsd);
}
}
void Application::ValidateOcioPreferences()
{
// A persisted display/view that doesn't exist in the active OCIO config
// makes HdxColorCorrectionTask throw ("Display 'x' not found") every frame
// and silently skip correction, which reads as a washed-out viewport --
// worst on delegates like hdEmbree whose output depends on the transform.
// Note display and view are easy to invert: in the bundled ACES 1.2 config
// the only display is "ACES" and "sRGB" is one of its views.
const OcioConfig& cfg = GetCurrentOcioConfig();
if (!cfg.valid) {
// No $OCIO / unreadable config -- nothing to validate against. Leave
// the prefs alone rather than clobbering values that may be correct
// for a config supplied later.
return;
}
auto contains = [](const std::vector<std::string>& v, const std::string& s) {
return std::find(v.begin(), v.end(), s) != v.end();
};
// --- display ---
if (m_prefs.ocioDisplay.empty() || !contains(cfg.displays, m_prefs.ocioDisplay)) {
if (!m_prefs.ocioDisplay.empty()) {
LOG_WARNING("OCIO display '" + m_prefs.ocioDisplay
+ "' not in config; falling back to '"
+ cfg.defaultDisplay + "'");
}
m_prefs.ocioDisplay = cfg.defaultDisplay;
}
// --- view (must belong to the display resolved above) ---
auto viewsIt = cfg.views.find(m_prefs.ocioDisplay);
const std::vector<std::string>* views =
(viewsIt != cfg.views.end()) ? &viewsIt->second : nullptr;
if (views && !views->empty()) {
if (m_prefs.ocioView.empty() || !contains(*views, m_prefs.ocioView)) {
// Prefer the config default when it's valid for this display,
// otherwise take the display's first view.
const std::string fallback =
contains(*views, cfg.defaultView) ? cfg.defaultView : views->front();
if (!m_prefs.ocioView.empty()) {
LOG_WARNING("OCIO view '" + m_prefs.ocioView
+ "' not valid for display '" + m_prefs.ocioDisplay
+ "'; falling back to '" + fallback + "'");
}
m_prefs.ocioView = fallback;
}
}
// --- color space (same failure mode if it names a missing space) ---
if (!m_prefs.ocioColorSpace.empty()
&& !contains(cfg.colorSpaces, m_prefs.ocioColorSpace)) {
LOG_WARNING("OCIO color space '" + m_prefs.ocioColorSpace
+ "' not in config; clearing it");
m_prefs.ocioColorSpace.clear();
}
// --- look ---
if (!m_prefs.ocioLook.empty()
&& !contains(cfg.looks, m_prefs.ocioLook)) {
LOG_WARNING("OCIO look '" + m_prefs.ocioLook
+ "' not in config; clearing it");
m_prefs.ocioLook.clear();
}
LOG_INFO("OCIO display/view: '" + m_prefs.ocioDisplay + "' / '"
+ m_prefs.ocioView + "'");
} }
void Application::SavePreferences() void Application::SavePreferences()
@@ -455,6 +570,7 @@ void Application::SavePreferences()
f << "OcioLook=" << m_prefs.ocioLook << "\n"; f << "OcioLook=" << m_prefs.ocioLook << "\n";
f << "MaterialBrowserWidth=" << m_prefs.materialBrowserWidth << "\n"; f << "MaterialBrowserWidth=" << m_prefs.materialBrowserWidth << "\n";
f << "MaterialPreviewWidth=" << m_prefs.materialPreviewWidth << "\n"; f << "MaterialPreviewWidth=" << m_prefs.materialPreviewWidth << "\n";
f << "KeepGraphNodeViewSettingsInUsd=" << (m_prefs.keepGraphNodeViewSettingsInUsd ? 1 : 0) << "\n";
} }
void Application::ApplyPrefsToAllViewports() void Application::ApplyPrefsToAllViewports()
@@ -505,6 +621,23 @@ void Application::RenderPreferencesDialog()
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
// ── Material ────────────────────────────────────────────────────────
if (ImGui::BeginTabItem("Material")) {
ImGui::Spacing();
if (ImGui::Checkbox("Keep graph node view settings in USD customData",
&m_prefs.keepGraphNodeViewSettingsInUsd)) {
if (m_materialEditorPanel)
m_materialEditorPanel->SetKeepGraphNodeViewSettingsInUsd(
m_prefs.keepGraphNodeViewSettingsInUsd);
}
ImGui::TextWrapped(
"When enabled, each node's pin display mode (all pins vs. "
"connected only) and any manually revealed pins are saved into "
"the material file and restored when it's reopened. When "
"disabled, this is a session-only view setting.");
ImGui::EndTabItem();
}
// ── Viewport ──────────────────────────────────────────────────────── // ── Viewport ────────────────────────────────────────────────────────
if (ImGui::BeginTabItem("Viewport")) { if (ImGui::BeginTabItem("Viewport")) {
ImGui::Spacing(); ImGui::Spacing();
@@ -743,6 +876,7 @@ void Application::RenderMenuBar() {
if (ImGui::BeginMenu("View")) { if (ImGui::BeginMenu("View")) {
ImGui::MenuItem("Stage Editor", nullptr, &m_showStageEditor); ImGui::MenuItem("Stage Editor", nullptr, &m_showStageEditor);
ImGui::MenuItem("Render Layers", nullptr, &m_showRenderLayerPanel);
ImGui::MenuItem("Scene Hierarchy", nullptr, &m_showSceneHierarchy); ImGui::MenuItem("Scene Hierarchy", nullptr, &m_showSceneHierarchy);
ImGui::MenuItem("Viewport", nullptr, &m_showViewport); ImGui::MenuItem("Viewport", nullptr, &m_showViewport);
ImGui::MenuItem("Property Panel", nullptr, &m_showPropertyPanel); ImGui::MenuItem("Property Panel", nullptr, &m_showPropertyPanel);
@@ -838,7 +972,9 @@ void Application::SaveUsdFile() {
if (!m_stageManager->SaveStage()) { if (!m_stageManager->SaveStage()) {
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError()); LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
return;
} }
SaveDirtyRenderLayers();
} }
void Application::SaveUsdFileAs() { void Application::SaveUsdFileAs() {
@@ -857,6 +993,11 @@ void Application::SaveUsdFileAs() {
if (!m_stageManager->SaveStageAs(filePath)) { if (!m_stageManager->SaveStageAs(filePath)) {
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError()); LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
} else { } else {
// m_renderLayerManager's layer refs are identifier-keyed SdfLayer
// objects, independent of which UsdStage currently references
// them — safe to save before the stage swap RefreshManagers()
// below performs.
SaveDirtyRenderLayers();
// SaveStageAs reopens m_stageManager's stage from the new file path. // SaveStageAs reopens m_stageManager's stage from the new file path.
// RefreshManagers syncs m_layerManager (and others) to that new stage; // RefreshManagers syncs m_layerManager (and others) to that new stage;
// without this, subsequent sublayer edits go to the old (now stale) stage // without this, subsequent sublayer edits go to the old (now stale) stage
@@ -866,6 +1007,15 @@ void Application::SaveUsdFileAs() {
} }
} }
void Application::SaveDirtyRenderLayers() {
if (!m_renderLayerManager) return;
for (const auto& info : m_renderLayerManager->GetRenderLayers()) {
if (info.isDefault || !info.layer) continue;
if (info.layer->IsDirty())
info.layer->Save();
}
}
void Application::CloseUsdFile() { void Application::CloseUsdFile() {
m_stageManager->CloseStage(); m_stageManager->CloseStage();
// Re-create a fresh default stage so the app is always in an editable state. // Re-create a fresh default stage so the app is always in an editable state.
+20
View File
@@ -4,6 +4,7 @@
#include "IconManager.h" #include "IconManager.h"
#include "StageEditorPanel.h" #include "StageEditorPanel.h"
#include "SceneHierarchyPanel.h" #include "SceneHierarchyPanel.h"
#include "RenderLayerPanel.h"
#include "ViewportPanel.h" #include "ViewportPanel.h"
#include "PropertyPanel.h" #include "PropertyPanel.h"
#include "TimelinePanel.h" #include "TimelinePanel.h"
@@ -11,6 +12,7 @@
#include "MaterialEditorPanel.h" #include "MaterialEditorPanel.h"
#include "../core/UsdStageManager.h" #include "../core/UsdStageManager.h"
#include "../core/LayerManager.h" #include "../core/LayerManager.h"
#include "../core/RenderLayerManager.h"
#include "../core/PropertyManager.h" #include "../core/PropertyManager.h"
#include "../core/MaterialManager.h" #include "../core/MaterialManager.h"
#include "../core/CommandHistory.h" #include "../core/CommandHistory.h"
@@ -30,6 +32,11 @@ struct AppPreferences {
std::string ocioLook; std::string ocioLook;
float materialBrowserWidth = 220.0f; ///< Material Editor left-column width float materialBrowserWidth = 220.0f; ///< Material Editor left-column width
float materialPreviewWidth = 320.0f; ///< Material Editor right-column width float materialPreviewWidth = 320.0f; ///< Material Editor right-column width
/// When true, per-node pin display mode (Hypershade-style "show all" vs
/// "show connected only") and revealed hidden pins are authored into USD
/// customData so they survive reopening the material; when false, that
/// state is session-only.
bool keepGraphNodeViewSettingsInUsd = false;
}; };
class Application { class Application {
@@ -60,12 +67,22 @@ private:
void AddReferenceToStage(); void AddReferenceToStage();
void CreatePrimOnStage(const std::string& typeName); void CreatePrimOnStage(const std::string& typeName);
/// Explicitly saves every render-layer SdfLayer, since at most one is
/// ever in the stage's composition (unmuted) at a time and
/// UsdStage::Save() only walks layers currently in composition — the
/// rest would otherwise be silently dropped on save.
void SaveDirtyRenderLayers();
// Playblast // Playblast
void CapturePlayblastFrame(); void CapturePlayblastFrame();
// Preferences // Preferences
void LoadPreferences(); void LoadPreferences();
void SavePreferences(); void SavePreferences();
/// Reconcile persisted OCIO display/view names against the active config.
/// Falls back to the config defaults when a name doesn't exist, so a stale
/// or mistyped preference can't silently disable color correction.
void ValidateOcioPreferences();
void ApplyPrefsToAllViewports(); void ApplyPrefsToAllViewports();
void RenderPreferencesDialog(); void RenderPreferencesDialog();
@@ -73,11 +90,13 @@ private:
std::unique_ptr<IconManager> m_iconManager; std::unique_ptr<IconManager> m_iconManager;
std::unique_ptr<UsdStageManager> m_stageManager; std::unique_ptr<UsdStageManager> m_stageManager;
std::unique_ptr<LayerManager> m_layerManager; std::unique_ptr<LayerManager> m_layerManager;
std::unique_ptr<RenderLayerManager> m_renderLayerManager;
std::unique_ptr<PropertyManager> m_propertyManager; std::unique_ptr<PropertyManager> m_propertyManager;
std::unique_ptr<MaterialManager> m_materialManager; std::unique_ptr<MaterialManager> m_materialManager;
CommandHistory m_commandHistory; CommandHistory m_commandHistory;
std::unique_ptr<StageEditorPanel> m_stageEditorPanel; std::unique_ptr<StageEditorPanel> m_stageEditorPanel;
std::unique_ptr<SceneHierarchyPanel> m_sceneHierarchyPanel; std::unique_ptr<SceneHierarchyPanel> m_sceneHierarchyPanel;
std::unique_ptr<RenderLayerPanel> m_renderLayerPanel;
std::unique_ptr<ViewportPanel> m_viewportPanel; std::unique_ptr<ViewportPanel> m_viewportPanel;
std::unique_ptr<PropertyPanel> m_propertyPanel; std::unique_ptr<PropertyPanel> m_propertyPanel;
std::unique_ptr<TimelinePanel> m_timelinePanel; std::unique_ptr<TimelinePanel> m_timelinePanel;
@@ -86,6 +105,7 @@ private:
bool m_showDemoWindow; bool m_showDemoWindow;
bool m_showStageInfo; bool m_showStageInfo;
bool m_showStageEditor; bool m_showStageEditor;
bool m_showRenderLayerPanel;
bool m_showSceneHierarchy; bool m_showSceneHierarchy;
bool m_showViewport; bool m_showViewport;
bool m_showPropertyPanel; bool m_showPropertyPanel;
+3
View File
@@ -61,6 +61,8 @@ static const char* IconFilename(Icon icon) {
case Icon::FilePlus: return "file-plus.svg"; case Icon::FilePlus: return "file-plus.svg";
case Icon::Pen: return "pen.svg"; case Icon::Pen: return "pen.svg";
case Icon::Settings: return "gear.svg"; case Icon::Settings: return "gear.svg";
case Icon::ChevronUp: return "chevron-up.svg";
case Icon::ChevronDown: return "chevron-down.svg";
default: return nullptr; default: return nullptr;
} }
} }
@@ -77,6 +79,7 @@ static constexpr Icon kAllIcons[] = {
Icon::SkipBack, Icon::StepBack, Icon::PlayBack, Icon::Play, Icon::Pause, Icon::SkipBack, Icon::StepBack, Icon::PlayBack, Icon::Play, Icon::Pause,
Icon::StepForward, Icon::SkipEnd, Icon::Loop, Icon::Bounce, Icon::StepForward, Icon::SkipEnd, Icon::Loop, Icon::Bounce,
Icon::Refresh, Icon::FilePlus, Icon::Pen, Icon::Settings, Icon::Refresh, Icon::FilePlus, Icon::Pen, Icon::Settings,
Icon::ChevronUp, Icon::ChevronDown,
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+3
View File
@@ -55,6 +55,9 @@ enum class Icon {
FilePlus, // create new file FilePlus, // create new file
Pen, // edit / edit target Pen, // edit / edit target
Settings, // gear / viewport options Settings, // gear / viewport options
// List reordering
ChevronUp, // move item up
ChevronDown, // move item down
}; };
/// Loads SVG files from disk, rasterizes them with NanoSVG, uploads them as /// Loads SVG files from disk, rasterizes them with NanoSVG, uploads them as
+481 -11
View File
@@ -8,6 +8,8 @@
#include "../core/commands/RenamePrimCommand.h" #include "../core/commands/RenamePrimCommand.h"
#include "../utils/Logger.h" #include "../utils/Logger.h"
#include "../utils/FileDialog.h" #include "../utils/FileDialog.h"
#include "../utils/OcioConfigParser.h"
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/usdShade/shader.h> #include <pxr/usd/usdShade/shader.h>
#include <pxr/usd/usdShade/material.h> #include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/materialBindingAPI.h> #include <pxr/usd/usdShade/materialBindingAPI.h>
@@ -45,6 +47,29 @@ uintptr_t HashId(const std::string& s) {
return static_cast<uintptr_t>(std::hash<std::string>{}(s)); return static_cast<uintptr_t>(std::hash<std::string>{}(s));
} }
// Draws the 3-dot pin-display-mode icon (filled-dot count = 3 - mode, so it
// visually "funnels down" as pins collapse further), centered within
// [min, max]. Shared by the per-node toggle in RenderNodeGraphCanvas (which
// sizes its button around the default dot geometry) and the toolbar's
// "set all nodes" button in RenderToolbar (which instead fits the dots into
// its fixed, already-square icon-button size via smaller dotRadius/dotSpacing).
void DrawPinDisplayModeIcon(ImDrawList* dl, const ImVec2& min, const ImVec2& max,
PinDisplayMode mode, ImU32 filledColor, ImU32 hollowColor,
float dotRadius = 3.0f, float dotSpacing = 9.0f) {
const int dotCount = 3;
const int filledDots = 3 - static_cast<int>(mode);
const float iconWidth = dotSpacing * (dotCount - 1) + dotRadius * 2.0f;
ImVec2 dotCenter(min.x + (max.x - min.x - iconWidth) * 0.5f + dotRadius,
min.y + (max.y - min.y) * 0.5f);
for (int i = 0; i < dotCount; ++i) {
if (i < filledDots)
dl->AddCircleFilled(dotCenter, dotRadius, filledColor);
else
dl->AddCircle(dotCenter, dotRadius, hollowColor, 0, 1.2f);
dotCenter.x += dotSpacing;
}
}
// Mirrors the file-local sanitizers in Application.cpp/SceneHierarchyPanel.cpp. // Mirrors the file-local sanitizers in Application.cpp/SceneHierarchyPanel.cpp.
std::string SanitizeUsdName(const std::string& raw) { std::string SanitizeUsdName(const std::string& raw) {
std::string result; std::string result;
@@ -306,7 +331,7 @@ void ApplyFallbackLayout(ShaderGraphSnapshot& graph) {
MaterialEditorPanel::MaterialEditorPanel() { MaterialEditorPanel::MaterialEditorPanel() {
// Route the vendored editor's click-time hit-test dump into our log so // Route the vendored editor's click-time hit-test dump into our log so
// it interleaves with the [NodeGraph] event trace. Disabled: uncomment to // it interleaves with the [NodeGraph] event trace. Disabled: uncomment to
// re-enable the [ed] trace while diagnosing the drag regression. // re-enable the [ed] trace when diagnosing canvas interaction issues.
// g_AxNodeEditorDebugLog = [](const char* msg) { LOG_INFO(std::string(msg)); }; // g_AxNodeEditorDebugLog = [](const char* msg) { LOG_INFO(std::string(msg)); };
NE::Config config; NE::Config config;
@@ -342,6 +367,14 @@ void MaterialEditorPanel::SetColorCorrectionFromPrefs(int ccMode, const std::str
const std::string& ocioColorSpace, const std::string& ocioColorSpace,
const std::string& ocioLook) { const std::string& ocioLook) {
m_preview.SetColorCorrection(ccMode, ocioDisplay, ocioView, ocioColorSpace, ocioLook); m_preview.SetColorCorrection(ccMode, ocioDisplay, ocioView, ocioColorSpace, ocioLook);
// Cached for the per-frame NodeThumbnailCache::SyncRenderSettings() call
// in RenderNodeGraphCanvas, which keeps in-canvas node thumbnails
// matching this same color correction (see its declaration).
m_ccMode = ccMode;
m_ocioDisplay = ocioDisplay;
m_ocioView = ocioView;
m_ocioColorSpace = ocioColorSpace;
m_ocioLook = ocioLook;
} }
void MaterialEditorPanel::Render() { void MaterialEditorPanel::Render() {
@@ -547,6 +580,18 @@ void MaterialEditorPanel::RenderSelectedNodeProperties() {
RenderInputValueWidget(*node, input, prim); RenderInputValueWidget(*node, input, prim);
ImGui::PopItemWidth(); ImGui::PopItemWidth();
} }
if (input.typeName == pxr::SdfValueTypeNames->Asset) {
ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);
ImGui::TableSetColumnIndex(1);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Color Space");
ImGui::TableSetColumnIndex(2);
ImGui::PushItemWidth(-FLT_MIN);
RenderColorSpaceWidget(*node, input, prim);
ImGui::PopItemWidth();
}
ImGui::PopID(); ImGui::PopID();
} }
ImGui::EndTable(); ImGui::EndTable();
@@ -622,12 +667,40 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
std::string s; std::string s;
if (current.IsHolding<std::string>()) s = current.UncheckedGet<std::string>(); if (current.IsHolding<std::string>()) s = current.UncheckedGet<std::string>();
else if (current.IsHolding<pxr::TfToken>()) s = current.UncheckedGet<pxr::TfToken>().GetString(); else if (current.IsHolding<pxr::TfToken>()) s = current.UncheckedGet<pxr::TfToken>().GetString();
if (!input.allowedValues.empty()) {
// Sdr-declared enum (e.g. UsdUVTexture's sourceColorSpace:
// raw/sRGB/auto) — a dropdown of exactly the legal values instead
// of free text, which could author anything the renderer ignores.
// A selection is one atomic edit (no drag-in-progress concept for
// a combo), so it's committed inline here — like the Asset browse
// button below — instead of through the drag/type commit
// machinery at the end of this function.
if (ImGui::BeginCombo(widgetId.c_str(), s.c_str())) {
for (const std::string& option : input.allowedValues) {
bool selected = (option == s);
if (ImGui::Selectable(option.c_str(), selected) && !selected) {
m_preEditValue = current;
m_preEditWasAuthored = authored;
pxr::UsdShadeShader shader(prim);
if (shader)
shader.CreateInput(pxr::TfToken(input.name), input.typeName)
.Set(type == tn->String ? pxr::VtValue(option) : pxr::VtValue(pxr::TfToken(option)));
CommitInputEdit(node, input);
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
return;
} else {
char buf[512]; char buf[512];
std::snprintf(buf, sizeof(buf), "%s", s.c_str()); std::snprintf(buf, sizeof(buf), "%s", s.c_str());
if (ImGui::InputText(widgetId.c_str(), buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) { if (ImGui::InputText(widgetId.c_str(), buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) {
if (type == tn->String) newValue = std::string(buf); if (type == tn->String) newValue = std::string(buf);
else newValue = pxr::TfToken(buf); else newValue = pxr::TfToken(buf);
} }
}
} else if (type == tn->Asset) { } else if (type == tn->Asset) {
std::string s; std::string s;
if (current.IsHolding<pxr::SdfAssetPath>()) s = current.UncheckedGet<pxr::SdfAssetPath>().GetAssetPath(); if (current.IsHolding<pxr::SdfAssetPath>()) s = current.UncheckedGet<pxr::SdfAssetPath>().GetAssetPath();
@@ -692,6 +765,64 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
CommitInputEdit(node, input); CommitInputEdit(node, input);
} }
void MaterialEditorPanel::RenderColorSpaceWidget(const ShaderGraphNode& node, const ShaderPinInfo& input,
const pxr::UsdPrim& prim) {
if (!m_stage || !m_commandHistory) return;
pxr::UsdAttribute attr = prim.GetAttribute(pxr::TfToken("inputs:" + input.name));
if (!attr) return;
const std::string current = attr.HasColorSpace() ? attr.GetColorSpace().GetString() : std::string();
// Listed from the active OCIO config ($OCIO — the same one driving
// viewport/preview color correction) rather than a fixed raw/sRGB/auto
// set: those are only meaningful to UsdUVTexture's own sourceColorSpace
// input, while this authors the renderer-agnostic colorSpace attribute
// metadata, so the choices should span whatever color spaces are
// actually defined and thus consistently interpretable across every
// render delegate (Storm, Arnold, Cycles, ...) rather than being tied to
// one shading backend's built-in vocabulary.
const OcioConfig& ocfg = GetCurrentOcioConfig();
static const std::vector<std::string> kFallback = {"raw", "sRGB"};
const std::vector<std::string>& colorSpaces = ocfg.valid && !ocfg.colorSpaces.empty()
? ocfg.colorSpaces : kFallback;
auto commit = [&](const std::string& newValue) {
pxr::UsdStageRefPtr stage = m_stage;
pxr::SdfPath nodePath = node.path;
pxr::TfToken attrName = attr.GetName();
std::string oldValue = current;
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
"Set color space on " + node.path.GetName() + "." + input.name,
[stage, nodePath, attrName, newValue]() {
pxr::UsdAttribute a = stage->GetPrimAtPath(nodePath).GetAttribute(attrName);
if (!a) return;
if (newValue.empty()) a.ClearColorSpace();
else a.SetColorSpace(pxr::TfToken(newValue));
},
[stage, nodePath, attrName, oldValue]() {
pxr::UsdAttribute a = stage->GetPrimAtPath(nodePath).GetAttribute(attrName);
if (!a) return;
if (oldValue.empty()) a.ClearColorSpace();
else a.SetColorSpace(pxr::TfToken(oldValue));
}));
};
if (ImGui::BeginCombo("##colorSpace", current.empty() ? "(unset)" : current.c_str())) {
if (ImGui::Selectable("(unset)", current.empty()) && !current.empty())
commit(std::string());
if (current.empty()) ImGui::SetItemDefaultFocus();
for (const std::string& cs : colorSpaces) {
const bool selected = (cs == current);
if (ImGui::Selectable(cs.c_str(), selected) && !selected)
commit(cs);
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered() && ocfg.valid)
ImGui::SetTooltip("Color spaces from the active OCIO config");
}
void MaterialEditorPanel::CommitInputEdit(const ShaderGraphNode& node, const ShaderPinInfo& input) { void MaterialEditorPanel::CommitInputEdit(const ShaderGraphNode& node, const ShaderPinInfo& input) {
if (!m_stage || !m_commandHistory) return; if (!m_stage || !m_commandHistory) return;
@@ -788,6 +919,50 @@ void MaterialEditorPanel::RenderToolbar() {
ImGui::EndPopup(); ImGui::EndPopup();
} }
ImGui::SameLine();
if (iconBtn("##toggleThumbs", m_showNodeThumbnails ? Icon::Eye : Icon::EyeSlash,
m_showNodeThumbnails ? "Hide Thumbnails" : "Show Thumbnails",
m_showNodeThumbnails ? "Hide node thumbnails" : "Show node thumbnails"))
m_showNodeThumbnails = !m_showNodeThumbnails;
// "Set all nodes" pin-display broadcast: same 3-dot icon language as the
// per-node toggle (DrawPinDisplayModeIcon), but hit-tested the normal
// ImGui way — this button lives in the toolbar window, not inside the
// node canvas, so none of RenderNodeGraphCanvas's geometric hit-test
// workarounds are needed here.
ImGui::SameLine();
ImGui::BeginDisabled(m_materialPath.IsEmpty());
{
ImGui::PushID("##allPinMode");
const ImVec2 pos = ImGui::GetCursorScreenPos();
const ImVec2 posMax(pos.x + btnH, pos.y + btnH);
const bool clicked = ImGui::InvisibleButton("##btn", ImVec2(btnH, btnH));
const bool hovered = ImGui::IsItemHovered();
ImDrawList* dl = ImGui::GetWindowDrawList();
const ImU32 bg = ImGui::GetColorU32(
hovered ? (ImGui::IsItemActive() ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered)
: ImGuiCol_Button);
dl->AddRectFilled(pos, posMax, bg, ImGui::GetStyle().FrameRounding);
// Dot geometry scaled to btnH (unlike the per-node toggle's fixed
// size) so the icon fits inside this fixed, already-square button
// instead of overflowing it.
DrawPinDisplayModeIcon(dl, pos, posMax, m_toolbarPinDisplayMode,
ImGui::GetColorU32(ImGuiCol_Text), ImGui::GetColorU32(ImGuiCol_TextDisabled),
btnH * 0.09f, btnH * 0.30f);
static const char* kTips[3] = {
"All nodes: showing all pins - click to show connected pins only on every node",
"All nodes: showing connected pins only - click to collapse every node to one pin",
"All nodes: collapsed to one pin per side - click to show all pins on every node",
};
if (hovered) ImGui::SetTooltip("%s", kTips[static_cast<int>(m_toolbarPinDisplayMode)]);
if (clicked) {
m_toolbarPinDisplayMode = static_cast<PinDisplayMode>((static_cast<int>(m_toolbarPinDisplayMode) + 1) % 3);
ApplyPinDisplayModeToAllNodes(m_toolbarPinDisplayMode);
}
ImGui::PopID();
}
ImGui::EndDisabled();
if (!m_targetPrimPath.IsEmpty()) { if (!m_targetPrimPath.IsEmpty()) {
ImGui::SameLine(); ImGui::SameLine();
ImGui::Text("Selected: %s", m_targetPrimPath.GetText()); ImGui::Text("Selected: %s", m_targetPrimPath.GetText());
@@ -1254,25 +1429,42 @@ void MaterialEditorPanel::BindMaterialToTarget(const pxr::SdfPath& materialPath,
} }
pxr::UsdStageRefPtr stage = m_stage; pxr::UsdStageRefPtr stage = m_stage;
auto doBind = [stage, targetPath, materialPath]() { // Captured at construction time so Undo() lands in the same layer as
// Execute() even if the ambient edit target changes in between (e.g. a
// render-layer switch) — mirrors ConnectShaderAttrsCommand's pattern.
pxr::SdfLayerHandle editLayer = m_stage->GetEditTarget().GetLayer();
auto doBind = [stage, targetPath, materialPath, editLayer]() {
pxr::UsdPrim target = stage->GetPrimAtPath(targetPath); pxr::UsdPrim target = stage->GetPrimAtPath(targetPath);
pxr::UsdShadeMaterial material(stage->GetPrimAtPath(materialPath)); pxr::UsdShadeMaterial material(stage->GetPrimAtPath(materialPath));
if (target && material) if (!target || !material) return;
if (editLayer) {
pxr::UsdEditContext ec(stage, editLayer);
pxr::UsdShadeMaterialBindingAPI::Apply(target).Bind(material); pxr::UsdShadeMaterialBindingAPI::Apply(target).Bind(material);
} else {
pxr::UsdShadeMaterialBindingAPI::Apply(target).Bind(material);
}
}; };
if (m_commandHistory) { if (m_commandHistory) {
m_commandHistory->Push(std::make_unique<AttributeSetCommand>( m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
"Bind Material", "Bind Material",
doBind, doBind,
[stage, targetPath, hadPriorRel, priorTargets]() { [stage, targetPath, hadPriorRel, priorTargets, editLayer]() {
pxr::UsdPrim target = stage->GetPrimAtPath(targetPath); pxr::UsdPrim target = stage->GetPrimAtPath(targetPath);
if (!target) return; if (!target) return;
auto doUnbind = [&]() {
pxr::UsdShadeMaterialBindingAPI bindAPI(target); pxr::UsdShadeMaterialBindingAPI bindAPI(target);
if (hadPriorRel && !priorTargets.empty()) if (hadPriorRel && !priorTargets.empty())
bindAPI.GetDirectBindingRel().SetTargets(priorTargets); bindAPI.GetDirectBindingRel().SetTargets(priorTargets);
else else
bindAPI.UnbindDirectBinding(); bindAPI.UnbindDirectBinding();
};
if (editLayer) {
pxr::UsdEditContext ec(stage, editLayer);
doUnbind();
} else {
doUnbind();
}
} }
)); ));
} else { } else {
@@ -1311,6 +1503,16 @@ void MaterialEditorPanel::SyncFromUsd() {
} }
m_graph = m_materialManager->GetShaderGraph(m_materialPath); m_graph = m_materialManager->GetShaderGraph(m_materialPath);
ApplyFallbackLayout(m_graph); ApplyFallbackLayout(m_graph);
SeedPinDisplayOverrides();
}
void MaterialEditorPanel::SeedPinDisplayOverrides() {
for (const auto& node : m_graph.nodes) {
const std::string key = node.path.GetString();
if (m_pinDisplayOverrides.find(key) != m_pinDisplayOverrides.end())
continue;
m_pinDisplayOverrides.emplace(key, PinDisplayOverride{node.pinDisplayMode});
}
} }
void MaterialEditorPanel::RenderNodeGraphCanvas() { void MaterialEditorPanel::RenderNodeGraphCanvas() {
@@ -1341,6 +1543,13 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
const ImVec2 viewCenterScreen(regionPos.x + regionSize.x * 0.5f, const ImVec2 viewCenterScreen(regionPos.x + regionSize.x * 0.5f,
regionPos.y + regionSize.y * 0.5f); regionPos.y + regionSize.y * 0.5f);
// Keep node thumbnails' renderer/color-correction matching the big
// preview swatch (cheap no-op unless something actually changed) before
// pumping, so a change this frame invalidates stale entries in time to
// be picked up by the pump below.
m_thumbnails.SyncRenderSettings(m_preview.GetRendererPlugin(), m_ccMode,
m_ocioDisplay, m_ocioView, m_ocioColorSpace, m_ocioLook);
// Drain finished texture decodes (GL upload), render one stale material // Drain finished texture decodes (GL upload), render one stale material
// thumbnail, and prune dead entries — GL/FBO work kept outside the // thumbnail, and prune dead entries — GL/FBO work kept outside the
// node-editor's Begin/End so it never touches mid-draw ImGui state. // node-editor's Begin/End so it never touches mid-draw ImGui state.
@@ -1365,6 +1574,12 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
// One whole-graph revision hash drives every material thumbnail this frame. // One whole-graph revision hash drives every material thumbnail this frame.
const size_t graphRevision = ComputeGraphRevision(m_stage, m_graph); const size_t graphRevision = ComputeGraphRevision(m_stage, m_graph);
// Tooltip requested by a hovered in-canvas widget this frame. Shown after
// NE::End(): inside the canvas io.MousePos is in canvas space, so a
// tooltip opened there is positioned at canvas coordinates misread as
// screen coordinates — deferring restores correct placement at the cursor.
const char* pendingTooltip = nullptr;
for (const auto& node : m_graph.nodes) { for (const auto& node : m_graph.nodes) {
uintptr_t nodeIdValue = HashId(node.path.GetString()); uintptr_t nodeIdValue = HashId(node.path.GetString());
NE::NodeId nodeId(nodeIdValue); NE::NodeId nodeId(nodeIdValue);
@@ -1380,9 +1595,11 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
// Thumbnail below the title: the source image for Texture nodes, a // Thumbnail below the title: the source image for Texture nodes, a
// shader-ball render for Material nodes. thumb is 0 until ready; the // shader-ball render for Material nodes. thumb is 0 until ready; the
// space is still reserved so the node doesn't jump when it appears. // space is still reserved so the node doesn't jump when it appears.
// Toggled off entirely (no reserved space, no decode/render work) via
// the toolbar's thumbnail eye icon.
ImTextureID thumb = 0; ImTextureID thumb = 0;
bool wantsThumb = false; bool wantsThumb = false;
if (node.category == "Texture") { if (m_showNodeThumbnails && node.category == "Texture") {
// Any texture node that exposes a file/asset input reserves a // Any texture node that exposes a file/asset input reserves a
// thumbnail slot. It stays black until a valid, on-disk image is // thumbnail slot. It stays black until a valid, on-disk image is
// decoded, so an unset or unresolvable path reads as a black chip. // decoded, so an unset or unresolvable path reads as a black chip.
@@ -1395,7 +1612,7 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
if (!file.empty()) if (!file.empty())
thumb = m_thumbnails.GetTextureThumbnail(node.path, file); thumb = m_thumbnails.GetTextureThumbnail(node.path, file);
} }
} else if (node.category == "Material") { } else if (m_showNodeThumbnails && node.category == "Material") {
wantsThumb = true; wantsThumb = true;
// Preview the terminal (token-typed) output as the surface; fall // Preview the terminal (token-typed) output as the surface; fall
// back to the first output routed into diffuseColor. // back to the first output routed into diffuseColor.
@@ -1414,18 +1631,88 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
// ImGui fork this project doesn't use) — approximate the classic // ImGui fork this project doesn't use) — approximate the classic
// "outputs hug the right edge" look by right-aligning each output row // "outputs hug the right edge" look by right-aligning each output row
// within the widest row measured across the whole node. // within the widest row measured across the whole node.
PinDisplayOverride& pinDisplay = GetPinDisplayOverride(node.path);
// Connected mode collapses every unconnected pin on a side into one
// connectable "more pins" nub (Maya Hypershade style), present only
// while that side actually has something hidden. Collapsed mode
// collapses every pin on a side — linked or not — into that nub
// whenever the side has any pins at all (existing links redirect to
// it too; see EffectivePinId()).
bool hasHiddenInput = false, hasHiddenOutput = false;
if (pinDisplay.mode == PinDisplayMode::Connected) {
for (const auto& input : node.inputs)
if (!IsPinLinked(node.path, input.name, false)) { hasHiddenInput = true; break; }
for (const auto& output : node.outputs)
if (!IsPinLinked(node.path, output.name, true)) { hasHiddenOutput = true; break; }
} else if (pinDisplay.mode == PinDisplayMode::Collapsed) {
hasHiddenInput = !node.inputs.empty();
hasHiddenOutput = !node.outputs.empty();
}
const float rowSpacing = ImGui::GetStyle().ItemSpacing.x; const float rowSpacing = ImGui::GetStyle().ItemSpacing.x;
float contentWidth = ImGui::CalcTextSize(title.c_str()).x; float contentWidth = ImGui::CalcTextSize(title.c_str()).x;
for (const auto& input : node.inputs) for (const auto& input : node.inputs)
if (IsPinVisible(node, input.name, false))
contentWidth = std::max(contentWidth, pinIconSize.x + rowSpacing + ImGui::CalcTextSize(input.name.c_str()).x); contentWidth = std::max(contentWidth, pinIconSize.x + rowSpacing + ImGui::CalcTextSize(input.name.c_str()).x);
for (const auto& output : node.outputs) for (const auto& output : node.outputs)
if (IsPinVisible(node, output.name, true))
contentWidth = std::max(contentWidth, pinIconSize.x + rowSpacing + ImGui::CalcTextSize(output.name.c_str()).x); contentWidth = std::max(contentWidth, pinIconSize.x + rowSpacing + ImGui::CalcTextSize(output.name.c_str()).x);
if (wantsThumb) if (wantsThumb)
contentWidth = std::max(contentWidth, static_cast<float>(NodeThumbnailCache::kThumbPx)); contentWidth = std::max(contentWidth, static_cast<float>(NodeThumbnailCache::kThumbPx));
NE::BeginNode(nodeId); NE::BeginNode(nodeId);
ImGui::PushID(node.path.GetText());
ImVec2 headerTop = ImGui::GetCursorScreenPos(); ImVec2 headerTop = ImGui::GetCursorScreenPos();
ImGui::TextUnformatted(title.c_str()); ImGui::TextUnformatted(title.c_str());
// Mode toggle, drawn and hit-tested manually. ImGui's own hover
// attribution is unreliable for widgets inside the canvas (the same
// mis-attribution the vendored editor's FindPinAt()/m_PressedNode
// patches work around for pins and node drags — the hover race is
// lost to the editor's background item), so an ImGui::SmallButton
// here never highlights or clicks on most nodes. Hit-test the rect
// geometrically instead, like those patches do. io.MousePos is in
// canvas space inside NE::Begin/End, the same space as the cursor
// position, so the comparison is direct; canvasHovered (screen-space,
// captured before NE::Begin) gates out clicks landing on other panels
// or popups, since the canvas-space transform is unclamped.
{
const ImGuiStyle& style = ImGui::GetStyle();
// Icon area sized to match the 3-dot icon's fixed geometry (see
// DrawPinDisplayModeIcon); filled-dot count visually "funnels
// down" as the node collapses further (All=3, Connected=2,
// Collapsed=1).
const ImVec2 iconSize(9.0f * 2.0f + 3.0f * 2.0f, 3.0f * 2.0f);
const ImVec2 btnMin = ImGui::GetCursorScreenPos();
const ImVec2 btnMax(btnMin.x + iconSize.x + style.FramePadding.x * 2.0f,
btnMin.y + iconSize.y + style.FramePadding.y * 2.0f);
ImGui::Dummy(ImVec2(btnMax.x - btnMin.x, btnMax.y - btnMin.y));
// IsMouseHoveringRect is pure geometry (no hover-id arbitration)
// and clips against the canvas view, so off-view parts don't hit.
const bool btnHovered = canvasHovered && ImGui::IsMouseHoveringRect(btnMin, btnMax);
const ImU32 frameColor = ImGui::GetColorU32(
btnHovered ? (ImGui::IsMouseDown(ImGuiMouseButton_Left) ? ImGuiCol_ButtonActive
: ImGuiCol_ButtonHovered)
: ImGuiCol_Button);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(btnMin, btnMax, frameColor, style.FrameRounding);
DrawPinDisplayModeIcon(dl, btnMin, btnMax, pinDisplay.mode,
ImGui::GetColorU32(ImGuiCol_Text), ImGui::GetColorU32(ImGuiCol_TextDisabled));
static const char* kTooltips[3] = {
"Showing all pins - click to show connected pins only",
"Showing connected pins only - click to collapse into one pin",
"Collapsed to one pin per side - click to show all pins",
};
if (btnHovered)
pendingTooltip = kTooltips[static_cast<int>(pinDisplay.mode)];
if (btnHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left))
CyclePinDisplayMode(node.path);
}
ImVec2 headerBottom = ImGui::GetItemRectMax(); ImVec2 headerBottom = ImGui::GetItemRectMax();
ImGui::Dummy(ImVec2(0.0f, 4.0f)); // breathing room between title and pins ImGui::Dummy(ImVec2(0.0f, 4.0f)); // breathing room between title and pins
@@ -1451,6 +1738,7 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
uintptr_t pinIdValue = HashId(node.path.GetString() + ":in:" + input.name); uintptr_t pinIdValue = HashId(node.path.GetString() + ":in:" + input.name);
m_pinIdToInfo[pinIdValue] = PinInfo{node.path, input.name, false, input.typeName}; m_pinIdToInfo[pinIdValue] = PinInfo{node.path, input.name, false, input.typeName};
bool linked = IsPinLinked(node.path, input.name, false); bool linked = IsPinLinked(node.path, input.name, false);
if (!IsPinVisible(node, input.name, false)) continue;
NE::BeginPin(NE::PinId(pinIdValue), NE::PinKind::Input); NE::BeginPin(NE::PinId(pinIdValue), NE::PinKind::Input);
// Default pivot is the center of the whole icon+text row, which // Default pivot is the center of the whole icon+text row, which
// makes links land mid-label instead of at the icon. Pin it to // makes links land mid-label instead of at the icon. Pin it to
@@ -1466,6 +1754,7 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
uintptr_t pinIdValue = HashId(node.path.GetString() + ":out:" + output.name); uintptr_t pinIdValue = HashId(node.path.GetString() + ":out:" + output.name);
m_pinIdToInfo[pinIdValue] = PinInfo{node.path, output.name, true, output.typeName}; m_pinIdToInfo[pinIdValue] = PinInfo{node.path, output.name, true, output.typeName};
bool linked = IsPinLinked(node.path, output.name, true); bool linked = IsPinLinked(node.path, output.name, true);
if (!IsPinVisible(node, output.name, true)) continue;
float rowWidth = ImGui::CalcTextSize(output.name.c_str()).x + rowSpacing + pinIconSize.x; float rowWidth = ImGui::CalcTextSize(output.name.c_str()).x + rowSpacing + pinIconSize.x;
if (contentWidth > rowWidth) if (contentWidth > rowWidth)
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (contentWidth - rowWidth)); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (contentWidth - rowWidth));
@@ -1479,6 +1768,38 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
NE::EndPin(); NE::EndPin();
} }
// Collapsed connector nubs on the node's bottom row (Connected or
// Collapsed mode): input nub at the left edge, output nub hugging the
// right edge. Dropping a connection on (or dragging one out of) a
// nub opens RenderPinPickMenu() to choose which pin it wires to.
if (hasHiddenInput || hasHiddenOutput) {
ImGui::Dummy(ImVec2(0.0f, 2.0f));
const float nubRowStartX = ImGui::GetCursorPosX();
if (hasHiddenInput) {
uintptr_t metaInId = HashId(node.path.GetString() + ":metaIn");
m_pinIdToInfo[metaInId] = PinInfo{node.path, "", false, pxr::SdfValueTypeName(), true};
NE::BeginPin(NE::PinId(metaInId), NE::PinKind::Input);
NE::PinPivotAlignment(ImVec2(0.0f, 0.5f));
NE::PinPivotSize(ImVec2(0.0f, 0.0f));
ax::Widgets::Icon(pinIconSize, ax::Widgets::IconType::Circle, false, ImColor(180, 180, 180, 255));
NE::EndPin();
if (ImGui::IsItemHovered()) pendingTooltip = "Connect to a hidden input";
}
if (hasHiddenOutput) {
if (hasHiddenInput) ImGui::SameLine();
ImGui::SetCursorPosX(nubRowStartX + contentWidth - pinIconSize.x);
uintptr_t metaOutId = HashId(node.path.GetString() + ":metaOut");
m_pinIdToInfo[metaOutId] = PinInfo{node.path, "", true, pxr::SdfValueTypeName(), true};
NE::BeginPin(NE::PinId(metaOutId), NE::PinKind::Output);
NE::PinPivotAlignment(ImVec2(1.0f, 0.5f));
NE::PinPivotSize(ImVec2(0.0f, 0.0f));
ax::Widgets::Icon(pinIconSize, ax::Widgets::IconType::Circle, false, ImColor(180, 180, 180, 255));
NE::EndPin();
if (ImGui::IsItemHovered()) pendingTooltip = "Connect a hidden output";
}
}
ImGui::PopID();
NE::EndNode(); NE::EndNode();
// Colored header strip drawn on the node's background draw list, // Colored header strip drawn on the node's background draw list,
@@ -1498,8 +1819,9 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
for (const auto& link : m_graph.links) { for (const auto& link : m_graph.links) {
uintptr_t linkIdValue = HashId(link.destNode.GetString() + ":" + link.destInput); uintptr_t linkIdValue = HashId(link.destNode.GetString() + ":" + link.destInput);
// The pin's own id, always populated in m_pinIdToInfo regardless of
// visibility (used here purely to color the link by the real type).
uintptr_t startPinId = HashId(link.sourceNode.GetString() + ":out:" + link.sourceOutput); uintptr_t startPinId = HashId(link.sourceNode.GetString() + ":out:" + link.sourceOutput);
uintptr_t endPinId = HashId(link.destNode.GetString() + ":in:" + link.destInput);
m_linkIdToInfo[linkIdValue] = LinkInfo{link.destNode, link.destInput}; m_linkIdToInfo[linkIdValue] = LinkInfo{link.destNode, link.destInput};
ImU32 linkColor = IM_COL32(200, 200, 200, 255); ImU32 linkColor = IM_COL32(200, 200, 200, 255);
@@ -1507,7 +1829,13 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
if (pinIt != m_pinIdToInfo.end()) if (pinIt != m_pinIdToInfo.end())
linkColor = GetPinColor(pinIt->second.typeName); linkColor = GetPinColor(pinIt->second.typeName);
NE::Link(NE::LinkId(linkIdValue), NE::PinId(startPinId), NE::PinId(endPinId), ImColor(linkColor)); // The id actually referenced by the drawn link: redirected to a
// node's collapsed meta nub when that node is in Collapsed mode
// (the real pin isn't drawn there, so its id can't be referenced).
uintptr_t linkStartPinId = EffectivePinId(link.sourceNode, link.sourceOutput, true);
uintptr_t linkEndPinId = EffectivePinId(link.destNode, link.destInput, false);
NE::Link(NE::LinkId(linkIdValue), NE::PinId(linkStartPinId), NE::PinId(linkEndPinId), ImColor(linkColor));
} }
HandleCreateAndDelete(); HandleCreateAndDelete();
@@ -1529,10 +1857,22 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
RenderNodeSearchMenu(NE::ScreenToCanvas(m_pendingCreateNodePos), &m_linkDragSourcePin); RenderNodeSearchMenu(NE::ScreenToCanvas(m_pendingCreateNodePos), &m_linkDragSourcePin);
ImGui::EndPopup(); ImGui::EndPopup();
} }
if (m_openPinPickMenu) {
ImGui::OpenPopup("PinPickMenu");
m_openPinPickMenu = false;
}
if (ImGui::BeginPopup("PinPickMenu")) {
RenderPinPickMenu();
ImGui::EndPopup();
}
NE::Resume(); NE::Resume();
NE::End(); NE::End();
// Deferred from the node loop — see pendingTooltip's declaration.
if (pendingTooltip)
ImGui::SetTooltip("%s", pendingTooltip);
// --- Debug tracing of node interaction events. Edge-triggered so each // --- Debug tracing of node interaction events. Edge-triggered so each
// event logs once, not per frame. Event order in the editor is: // event logs once, not per frame. Event order in the editor is:
// LMB press -> node becomes active (and is brought to front) -> drag // LMB press -> node becomes active (and is brought to front) -> drag
@@ -1723,8 +2063,20 @@ void MaterialEditorPanel::HandleCreateAndDelete() {
const PinInfo& b = endIt->second; const PinInfo& b = endIt->second;
const PinInfo& outPin = a.isOutput ? a : b; const PinInfo& outPin = a.isOutput ? a : b;
const PinInfo& inPin = a.isOutput ? b : a; const PinInfo& inPin = a.isOutput ? b : a;
if (outPin.isMeta || inPin.isMeta) {
// Dropped on (or dragged from) a collapsed "more pins"
// nub: defer to RenderPinPickMenu() to choose which
// actual pin it wires to, instead of connecting now.
const PinInfo& metaSide = outPin.isMeta ? outPin : inPin;
const PinInfo& otherSide = outPin.isMeta ? inPin : outPin;
m_pendingPinPick.nodePath = metaSide.nodePath;
m_pendingPinPick.isOutput = metaSide.isOutput;
m_pendingPinPick.otherPin = otherSide;
m_openPinPickMenu = true;
} else {
CreateConnection(inPin, outPin); CreateConnection(inPin, outPin);
} }
}
} else { } else {
NE::RejectNewItem(); NE::RejectNewItem();
} }
@@ -1735,10 +2087,13 @@ void MaterialEditorPanel::HandleCreateAndDelete() {
// and pop the create-node menu (deferred to the suspended block below, // and pop the create-node menu (deferred to the suspended block below,
// since OpenPopup must not run mid-editor); the chosen node is wired to // since OpenPopup must not run mid-editor); the chosen node is wired to
// this pin. // this pin.
// Dragging from a collapsed meta pin onto empty canvas is not
// supported (there's no single concrete pin/type to auto-wire the
// new node to) — left un-accepted here so the library cancels it.
NE::PinId newNodePinId; NE::PinId newNodePinId;
if (NE::QueryNewNode(&newNodePinId)) { if (NE::QueryNewNode(&newNodePinId)) {
auto it = m_pinIdToInfo.find(newNodePinId.Get()); auto it = m_pinIdToInfo.find(newNodePinId.Get());
if (it != m_pinIdToInfo.end() && NE::AcceptNewItem()) { if (it != m_pinIdToInfo.end() && !it->second.isMeta && NE::AcceptNewItem()) {
m_linkDragSourcePin = it->second; m_linkDragSourcePin = it->second;
m_openLinkDragMenu = true; m_openLinkDragMenu = true;
m_pendingCreateNodePos = ImGui::GetMousePos(); m_pendingCreateNodePos = ImGui::GetMousePos();
@@ -1779,6 +2134,87 @@ bool MaterialEditorPanel::IsPinLinked(const pxr::SdfPath& nodePath, const std::s
return false; return false;
} }
MaterialEditorPanel::PinDisplayOverride& MaterialEditorPanel::GetPinDisplayOverride(const pxr::SdfPath& nodePath) {
return m_pinDisplayOverrides[nodePath.GetString()];
}
bool MaterialEditorPanel::IsPinVisible(const ShaderGraphNode& node, const std::string& pinName, bool isOutput) const {
auto it = m_pinDisplayOverrides.find(node.path.GetString());
PinDisplayMode mode = (it != m_pinDisplayOverrides.end()) ? it->second.mode : PinDisplayMode::All;
switch (mode) {
case PinDisplayMode::All: return true;
case PinDisplayMode::Connected: return IsPinLinked(node.path, pinName, isOutput);
case PinDisplayMode::Collapsed: return false;
}
return true;
}
void MaterialEditorPanel::CyclePinDisplayMode(const pxr::SdfPath& nodePath) {
PinDisplayOverride& ov = GetPinDisplayOverride(nodePath);
switch (ov.mode) {
case PinDisplayMode::All: ov.mode = PinDisplayMode::Connected; break;
case PinDisplayMode::Connected: ov.mode = PinDisplayMode::Collapsed; break;
case PinDisplayMode::Collapsed: ov.mode = PinDisplayMode::All; break;
}
PersistPinDisplayState(nodePath);
}
void MaterialEditorPanel::ApplyPinDisplayModeToAllNodes(PinDisplayMode mode) {
for (const auto& node : m_graph.nodes) {
PinDisplayOverride& ov = GetPinDisplayOverride(node.path);
if (ov.mode != mode) {
ov.mode = mode;
PersistPinDisplayState(node.path);
}
}
}
uintptr_t MaterialEditorPanel::EffectivePinId(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const {
auto it = m_pinDisplayOverrides.find(nodePath.GetString());
if (it != m_pinDisplayOverrides.end() && it->second.mode == PinDisplayMode::Collapsed)
return HashId(nodePath.GetString() + (isOutput ? ":metaOut" : ":metaIn"));
return HashId(nodePath.GetString() + (isOutput ? ":out:" : ":in:") + pinName);
}
void MaterialEditorPanel::RenderPinPickMenu() {
const ShaderGraphNode* node = nullptr;
for (const auto& n : m_graph.nodes)
if (n.path == m_pendingPinPick.nodePath) { node = &n; break; }
if (!node) return;
const bool isOutput = m_pendingPinPick.isOutput;
const std::vector<ShaderPinInfo>& pins = isOutput ? node->outputs : node->inputs;
ImGui::TextDisabled(isOutput ? "Connect output" : "Connect input");
ImGui::Separator();
for (const auto& pin : pins) {
if (IsPinLinked(node->path, pin.name, isOutput)) continue; // already wired elsewhere
if (ImGui::MenuItem(pin.name.c_str())) {
CompletePinPick(PinInfo{node->path, pin.name, isOutput, pin.typeName, false});
ImGui::CloseCurrentPopup();
break;
}
}
}
void MaterialEditorPanel::CompletePinPick(const PinInfo& chosen) {
if (m_pendingPinPick.otherPin.isMeta) {
// Dragged directly between two collapsed nubs: this pick resolved
// one side to a real pin, now chain into a second pick for the far
// side (deferred, since we're inside the already-open popup).
PendingPinPick next;
next.nodePath = m_pendingPinPick.otherPin.nodePath;
next.isOutput = m_pendingPinPick.otherPin.isOutput;
next.otherPin = chosen;
m_pendingPinPick = next;
m_openPinPickMenu = true;
return;
}
const PinInfo& outPin = chosen.isOutput ? chosen : m_pendingPinPick.otherPin;
const PinInfo& inPin = chosen.isOutput ? m_pendingPinPick.otherPin : chosen;
CreateConnection(inPin, outPin);
}
void MaterialEditorPanel::RenderNodeSearchMenu(const ImVec2& canvasPos, const PinInfo* linkSource) { void MaterialEditorPanel::RenderNodeSearchMenu(const ImVec2& canvasPos, const PinInfo* linkSource) {
if (!m_materialManager || !m_stage || m_materialPath.IsEmpty()) { if (!m_materialManager || !m_stage || m_materialPath.IsEmpty()) {
ImGui::TextDisabled("Open or create a material first"); ImGui::TextDisabled("Open or create a material first");
@@ -1937,9 +2373,13 @@ pxr::SdfPath MaterialEditorPanel::CreateShaderNode(const std::string& shaderId,
void MaterialEditorPanel::CreateConnection(const PinInfo& destInput, const PinInfo& sourceOutput) { void MaterialEditorPanel::CreateConnection(const PinInfo& destInput, const PinInfo& sourceOutput) {
if (!m_stage || !m_commandHistory) return; if (!m_stage || !m_commandHistory) return;
// Captured now so Undo() (which may run after the ambient edit target
// has since changed, e.g. a render-layer switch) still targets the same
// layer this connection was authored into.
pxr::SdfLayerHandle editLayer = m_stage->GetEditTarget().GetLayer();
m_commandHistory->Push(std::make_unique<ConnectShaderAttrsCommand>( m_commandHistory->Push(std::make_unique<ConnectShaderAttrsCommand>(
m_stage, destInput.nodePath, destInput.name, destInput.typeName, m_stage, destInput.nodePath, destInput.name, destInput.typeName,
sourceOutput.nodePath, sourceOutput.name, sourceOutput.typeName)); sourceOutput.nodePath, sourceOutput.name, sourceOutput.typeName, editLayer));
SyncFromUsd(); SyncFromUsd();
} }
@@ -1951,7 +2391,8 @@ void MaterialEditorPanel::DeleteNode(const pxr::SdfPath& nodePath) {
void MaterialEditorPanel::DisconnectAttr(const pxr::SdfPath& destNode, const std::string& destInput) { void MaterialEditorPanel::DisconnectAttr(const pxr::SdfPath& destNode, const std::string& destInput) {
if (!m_stage || !m_commandHistory) return; if (!m_stage || !m_commandHistory) return;
m_commandHistory->Push(std::make_unique<DisconnectShaderAttrCommand>(m_stage, destNode, destInput)); pxr::SdfLayerHandle editLayer = m_stage->GetEditTarget().GetLayer();
m_commandHistory->Push(std::make_unique<DisconnectShaderAttrCommand>(m_stage, destNode, destInput, editLayer));
SyncFromUsd(); SyncFromUsd();
} }
@@ -2038,6 +2479,35 @@ void MaterialEditorPanel::PersistNodePosition(NE::NodeId nodeId) {
})); }));
} }
void MaterialEditorPanel::PersistPinDisplayState(const pxr::SdfPath& nodePath) {
if (!m_keepGraphNodeViewSettingsInUsd || !m_stage || !m_commandHistory) return;
pxr::UsdPrim prim = m_stage->GetPrimAtPath(nodePath);
if (!prim.IsValid()) return;
static const pxr::TfToken kPinModeKey("uiPinDisplayMode");
const int newMode = static_cast<int>(GetPinDisplayOverride(nodePath).mode);
pxr::VtValue oldModeVt = prim.GetCustomDataByKey(kPinModeKey);
const int oldMode = oldModeVt.IsHolding<int>() ? oldModeVt.UncheckedGet<int>() : static_cast<int>(PinDisplayMode::All);
if (oldMode == newMode)
return;
pxr::UsdStageRefPtr stage = m_stage;
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
"Change pin display for " + nodePath.GetName(),
[stage, nodePath, newMode]() {
pxr::UsdPrim p = stage->GetPrimAtPath(nodePath);
if (p.IsValid()) p.SetCustomDataByKey(pxr::TfToken("uiPinDisplayMode"), pxr::VtValue(newMode));
},
[stage, nodePath, oldMode]() {
pxr::UsdPrim p = stage->GetPrimAtPath(nodePath);
if (p.IsValid()) p.SetCustomDataByKey(pxr::TfToken("uiPinDisplayMode"), pxr::VtValue(oldMode));
}));
}
bool MaterialEditorPanel::SaveNodeSettingsCallback(NE::NodeId nodeId, const char* /*data*/, size_t /*size*/, bool MaterialEditorPanel::SaveNodeSettingsCallback(NE::NodeId nodeId, const char* /*data*/, size_t /*size*/,
NE::SaveReasonFlags reason, void* userPointer) { NE::SaveReasonFlags reason, void* userPointer) {
auto* self = static_cast<MaterialEditorPanel*>(userPointer); auto* self = static_cast<MaterialEditorPanel*>(userPointer);
+107
View File
@@ -54,6 +54,12 @@ public:
} }
void SetColumnWidths(float browserWidth, float previewWidth); void SetColumnWidths(float browserWidth, float previewWidth);
/// Whether toggling a node's pin display mode / revealing a hidden pin
/// authors USD customData (persisted) or stays session-only. Pushed in by
/// Application from AppPreferences, live-updated when the user flips the
/// checkbox in Preferences -> Material.
void SetKeepGraphNodeViewSettingsInUsd(bool value) { m_keepGraphNodeViewSettingsInUsd = value; }
void Render(); void Render();
private: private:
@@ -63,6 +69,12 @@ private:
std::string name; std::string name;
bool isOutput = false; bool isOutput = false;
pxr::SdfValueTypeName typeName; pxr::SdfValueTypeName typeName;
/// True for the collapsed "more pins" connector nub rendered on a
/// node's title row when it's hiding unconnected pins (Hypershade
/// style) — name/typeName are unset. Connecting to/from it opens
/// RenderPinPickMenu() to choose which actual pin to wire, instead of
/// creating the connection immediately. See HandleCreateAndDelete().
bool isMeta = false;
}; };
/// Resolved endpoint of a rendered link, keyed by its ax::NodeEditor LinkId. /// Resolved endpoint of a rendered link, keyed by its ax::NodeEditor LinkId.
struct LinkInfo { struct LinkInfo {
@@ -70,6 +82,24 @@ private:
std::string destInput; std::string destInput;
}; };
/// Hypershade-style per-node pin display state. Always the render-time
/// source of truth (never resync-clobbered); optionally mirrored into USD
/// customData when m_keepGraphNodeViewSettingsInUsd is set. See
/// GetPinDisplayOverride()/IsPinVisible().
struct PinDisplayOverride {
PinDisplayMode mode = PinDisplayMode::All;
};
/// A connection dragged onto/from a collapsed meta pin, awaiting the user
/// picking which actual pin on nodePath/isOutput to wire up. otherPin is
/// the already-resolved endpoint on the far side of the link (itself meta
/// if the user dragged directly between two collapsed nubs, in which case
/// picking chains into a second RenderPinPickMenu() for that side).
struct PendingPinPick {
pxr::SdfPath nodePath;
bool isOutput = false;
PinInfo otherPin;
};
void RenderToolbar(); void RenderToolbar();
/// Property editor for the node selected in the graph, shown below the /// Property editor for the node selected in the graph, shown below the
/// shader-ball preview. Values apply live while a widget is being /// shader-ball preview. Values apply live while a widget is being
@@ -78,6 +108,14 @@ private:
void RenderInputValueWidget(const ShaderGraphNode& node, const ShaderPinInfo& input, void RenderInputValueWidget(const ShaderGraphNode& node, const ShaderPinInfo& input,
const pxr::UsdPrim& prim); const pxr::UsdPrim& prim);
void CommitInputEdit(const ShaderGraphNode& node, const ShaderPinInfo& input); void CommitInputEdit(const ShaderGraphNode& node, const ShaderPinInfo& input);
/// A "Color Space" row (raw/sRGB/auto/unset) under an Asset-typed input,
/// authoring the attribute's colorSpace metadata (UsdAttribute::
/// SetColorSpace) — the generic, renderer-agnostic mechanism Hydra reads
/// for texture color space (see UsdImagingDataSourceAttributeColorSpace),
/// and the only one available for shader types with no separate
/// sourceColorSpace input, e.g. MaterialX image nodes.
void RenderColorSpaceWidget(const ShaderGraphNode& node, const ShaderPinInfo& input,
const pxr::UsdPrim& prim);
/// Hypershade-style browser: lists every material on the stage; the /// Hypershade-style browser: lists every material on the stage; the
/// selected one is loaded into the graph work area via "Show Graph" /// selected one is loaded into the graph work area via "Show Graph"
/// (or double-click). /// (or double-click).
@@ -122,6 +160,45 @@ private:
void PersistNodePosition(ax::NodeEditor::NodeId nodeId); void PersistNodePosition(ax::NodeEditor::NodeId nodeId);
bool IsPinLinked(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const; bool IsPinLinked(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const;
/// Seeds m_pinDisplayOverrides for any node path not already present
/// (i.e. not touched yet this session) from the just-synced m_graph's
/// pinDisplayMode (customData-backed). Never overwrites an existing
/// entry, so in-session toggles survive resyncs.
void SeedPinDisplayOverrides();
/// Creates the entry (defaulted to All) on first access.
PinDisplayOverride& GetPinDisplayOverride(const pxr::SdfPath& nodePath);
/// True if pin should be drawn this frame: All mode shows everything;
/// Connected mode shows only linked pins (linked pins are never hidden —
/// Link() unconditionally references pin IDs that must have been drawn —
/// so a hidden pin becomes visible the moment it's connected, via
/// RenderPinPickMenu()); Collapsed mode hides every real pin, linked or
/// not, in favor of the single connector nub per side (see
/// EffectivePinId(), which redirects existing links to that nub).
bool IsPinVisible(const ShaderGraphNode& node, const std::string& pinName, bool isOutput) const;
/// Cycles a node's display mode: All -> Connected -> Collapsed -> All.
void CyclePinDisplayMode(const pxr::SdfPath& nodePath);
/// Sets every node currently in m_graph to mode (toolbar "set all" button).
void ApplyPinDisplayModeToAllNodes(PinDisplayMode mode);
/// Authors the node's current PinDisplayOverride into USD customData via
/// an undoable command, mirroring PersistNodePosition. No-op unless
/// m_keepGraphNodeViewSettingsInUsd is set.
void PersistPinDisplayState(const pxr::SdfPath& nodePath);
/// PinId to use when drawing a link touching (nodePath, pinName,
/// isOutput): the pin's own id normally, or that node's collapsed meta
/// nub id when the node is in Collapsed mode (so existing links visually
/// converge on the nub instead of referencing an undrawn real pin).
uintptr_t EffectivePinId(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const;
/// Renders the deferred "choose pin" popup for m_pendingPinPick (Maya
/// Hypershade style): a connection was dragged onto/from a collapsed meta
/// pin, and the user now picks which actual attribute it wires to. Must
/// be called from within the NE::Suspend()/Resume() block, like the
/// other popups.
void RenderPinPickMenu();
/// Finishes a pick: creates the real connection to/from chosen, unless
/// the far side of the pending pick was itself a meta pin, in which case
/// this chains into a second pick for that side instead.
void CompletePinPick(const PinInfo& chosen);
static bool SaveNodeSettingsCallback(ax::NodeEditor::NodeId nodeId, static bool SaveNodeSettingsCallback(ax::NodeEditor::NodeId nodeId,
const char* data, size_t size, const char* data, size_t size,
ax::NodeEditor::SaveReasonFlags reason, ax::NodeEditor::SaveReasonFlags reason,
@@ -133,6 +210,15 @@ private:
CommandHistory* m_commandHistory = nullptr; CommandHistory* m_commandHistory = nullptr;
IconManager* m_iconManager = nullptr; IconManager* m_iconManager = nullptr;
/// Last color correction pushed via SetColorCorrectionFromPrefs, cached
/// so RenderNodeGraphCanvas can pass it to NodeThumbnailCache every frame
/// (keeps in-canvas node thumbnails matching m_preview/the app global).
int m_ccMode = 1; // ColorCorrectionMode cast to int (1 = sRGB), mirrors AppPreferences' default
std::string m_ocioDisplay;
std::string m_ocioView;
std::string m_ocioColorSpace;
std::string m_ocioLook;
/// Material currently shown in the graph work area (empty = none). /// Material currently shown in the graph work area (empty = none).
pxr::SdfPath m_materialPath; pxr::SdfPath m_materialPath;
/// Material highlighted in the browser list; becomes m_materialPath when /// Material highlighted in the browser list; becomes m_materialPath when
@@ -170,6 +256,27 @@ private:
bool m_openLinkDragMenu = false; bool m_openLinkDragMenu = false;
PinInfo m_linkDragSourcePin; PinInfo m_linkDragSourcePin;
/// Hypershade-style pin display state, keyed by node path string. Always
/// the render-time source of truth; see PinDisplayOverride.
std::unordered_map<std::string, PinDisplayOverride> m_pinDisplayOverrides;
/// Whether node toggles author USD customData (see
/// SetKeepGraphNodeViewSettingsInUsd).
bool m_keepGraphNodeViewSettingsInUsd = false;
/// Deferred "choose pin" popup request: set from HandleCreateAndDelete()
/// when a link is dropped on/dragged from a collapsed meta pin, actually
/// opened after NE::Suspend(), like m_openLinkDragMenu.
bool m_openPinPickMenu = false;
PendingPinPick m_pendingPinPick;
/// Toolbar toggle: whether node thumbnails (texture images / material
/// shader-ball renders) are shown at all. Off skips the reserved space
/// and the decode/render work entirely, not just the visual.
bool m_showNodeThumbnails = true;
/// Toolbar "set all nodes" pin-display button: the mode last broadcast
/// to every node (and the icon/tooltip shown), independent of what any
/// individual node is actually in after the user tweaks one by hand.
PinDisplayMode m_toolbarPinDisplayMode = PinDisplayMode::All;
/// Column widths of the browser (left) and preview (right) sections, /// Column widths of the browser (left) and preview (right) sections,
/// user-adjustable via the splitters between them and the canvas. /// user-adjustable via the splitters between them and the canvas.
float m_browserWidth = 220.0f; float m_browserWidth = 220.0f;
+13 -5
View File
@@ -592,6 +592,17 @@ void MaterialPreviewRenderer::SetColorCorrection(int ccMode, const std::string&
m_dirty = true; m_dirty = true;
} }
void MaterialPreviewRenderer::SetRendererPlugin(const pxr::TfToken& pluginId) {
if (pluginId == m_renderer.GetCurrentRendererId()) return;
m_renderer.SetRendererPlugin(pluginId);
UpdateDomeOrientation(); // pole convention differs per delegate
m_dirty = true;
}
pxr::TfToken MaterialPreviewRenderer::GetRendererPlugin() const {
return m_renderer.GetCurrentRendererId();
}
void MaterialPreviewRenderer::RenderRendererDropdown(float width) { void MaterialPreviewRenderer::RenderRendererDropdown(float width) {
pxr::TfToken currentId = m_renderer.GetCurrentRendererId(); pxr::TfToken currentId = m_renderer.GetCurrentRendererId();
std::string displayName = currentId.IsEmpty() std::string displayName = currentId.IsEmpty()
@@ -607,11 +618,8 @@ void MaterialPreviewRenderer::RenderRendererDropdown(float width) {
std::string name = UsdSceneRenderer::GetRendererDisplayName(pluginId); std::string name = UsdSceneRenderer::GetRendererDisplayName(pluginId);
if (name.empty()) name = pluginId.GetString(); if (name.empty()) name = pluginId.GetString();
bool selected = (pluginId == currentId); bool selected = (pluginId == currentId);
if (ImGui::MenuItem(name.c_str(), nullptr, selected) && !selected) { if (ImGui::MenuItem(name.c_str(), nullptr, selected) && !selected)
m_renderer.SetRendererPlugin(pluginId); SetRendererPlugin(pluginId);
UpdateDomeOrientation(); // pole convention differs per delegate
m_dirty = true;
}
} }
ImGui::EndPopup(); ImGui::EndPopup();
} }
+16
View File
@@ -49,6 +49,22 @@ public:
const std::string& ocioColorSpace, const std::string& ocioColorSpace,
const std::string& ocioLook); const std::string& ocioLook);
/// Switches the shader-ball's render delegate (what RenderRendererDropdown
/// does on click). No-op if already this plugin. Exposed so other
/// consumers of a material preview (e.g. NodeThumbnailCache's in-canvas
/// thumbnails) can mirror this renderer's current choice.
void SetRendererPlugin(const pxr::TfToken& pluginId);
/// Current render delegate (empty until EnsureInitialized() has run).
pxr::TfToken GetRendererPlugin() const;
/// True once the last Render() call's image is final. Progressive
/// delegates (Arnold/Cycles/Embree) return false for many calls after a
/// material/renderer change while they accumulate samples — callers that
/// cache the rendered texture (e.g. NodeThumbnailCache) must keep
/// re-rendering until this is true, or they'll cache an early noisy (or
/// still-black) frame.
bool IsConverged() const { return m_renderer.IsConverged(); }
/// Renderer-delegate picker UI (matches the viewport's dropdown pattern). /// Renderer-delegate picker UI (matches the viewport's dropdown pattern).
/// width: ImGui item width (-1 = fill available). /// width: ImGui item width (-1 = fill available).
void RenderRendererDropdown(float width = -1.0f); void RenderRendererDropdown(float width = -1.0f);
+74
View File
@@ -94,6 +94,7 @@ bool ImGuiEx::Canvas::Begin(ImGuiID id, const ImVec2& size)
# endif # endif
SaveInputState(); SaveInputState();
SaveViewportState();
EnterLocalSpace(); EnterLocalSpace();
@@ -319,6 +320,46 @@ void ImGuiEx::Canvas::RestoreInputState()
ImGui::GetCurrentWindow()->DC.CursorMaxPos = m_WindowCursorMaxBackup; ImGui::GetCurrentWindow()->DC.CursorMaxPos = m_WindowCursorMaxBackup;
} }
// UsdLayerManager local patch, ported from upstream imgui-node-editor
// (see the declaration in imgui_canvas.h for why).
void ImGuiEx::Canvas::SaveViewportState()
{
# if defined(IMGUI_HAS_VIEWPORT)
auto window = ImGui::GetCurrentWindow();
auto viewport = ImGui::GetWindowViewport();
m_WindowPosBackup = window->Pos;
m_ViewportPosBackup = viewport->Pos;
m_ViewportSizeBackup = viewport->Size;
# if IMGUI_VERSION_NUM > 18002
m_ViewportWorkPosBackup = viewport->WorkPos;
m_ViewportWorkSizeBackup = viewport->WorkSize;
# else
m_ViewportWorkOffsetMinBackup = viewport->WorkOffsetMin;
m_ViewportWorkOffsetMaxBackup = viewport->WorkOffsetMax;
# endif
# endif
}
void ImGuiEx::Canvas::RestoreViewportState()
{
# if defined(IMGUI_HAS_VIEWPORT)
auto window = ImGui::GetCurrentWindow();
auto viewport = ImGui::GetWindowViewport();
window->Pos = m_WindowPosBackup;
viewport->Pos = m_ViewportPosBackup;
viewport->Size = m_ViewportSizeBackup;
# if IMGUI_VERSION_NUM > 18002
viewport->WorkPos = m_ViewportWorkPosBackup;
viewport->WorkSize = m_ViewportWorkSizeBackup;
# else
viewport->WorkOffsetMin = m_ViewportWorkOffsetMinBackup;
viewport->WorkOffsetMax = m_ViewportWorkOffsetMaxBackup;
# endif
# endif
}
void ImGuiEx::Canvas::EnterLocalSpace() void ImGuiEx::Canvas::EnterLocalSpace()
{ {
// Prepare ImDrawList for drawing in local coordinate system: // Prepare ImDrawList for drawing in local coordinate system:
@@ -374,6 +415,38 @@ void ImGuiEx::Canvas::EnterLocalSpace()
for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i) for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i)
io.MouseClickedPos[i] = (m_MouseClickedPosBackup[i] - m_ViewTransformPosition) * m_View.InvScale; io.MouseClickedPos[i] = (m_MouseClickedPosBackup[i] - m_ViewTransformPosition) * m_View.InvScale;
// UsdLayerManager local patch, ported from upstream imgui-node-editor:
// move the viewport rect into local space too, so IsMouseHoveringRect's
// screen-space viewport overlap check (docking-branch ImGui) passes for
// items at canvas coordinates outside the screen range (e.g. negative).
// See SaveViewportState() declaration for the full story.
# if defined(IMGUI_HAS_VIEWPORT)
{
auto window = ImGui::GetCurrentWindow();
window->Pos = ImVec2(0.0f, 0.0f);
auto viewport_min = m_ViewportPosBackup;
auto viewport_max = m_ViewportPosBackup + m_ViewportSizeBackup;
viewport_min.x = (viewport_min.x - m_ViewTransformPosition.x) * m_View.InvScale;
viewport_min.y = (viewport_min.y - m_ViewTransformPosition.y) * m_View.InvScale;
viewport_max.x = (viewport_max.x - m_ViewTransformPosition.x) * m_View.InvScale;
viewport_max.y = (viewport_max.y - m_ViewTransformPosition.y) * m_View.InvScale;
auto viewport = ImGui::GetWindowViewport();
viewport->Pos = viewport_min;
viewport->Size = viewport_max - viewport_min;
# if IMGUI_VERSION_NUM > 18002
viewport->WorkPos = m_ViewportWorkPosBackup * m_View.InvScale;
viewport->WorkSize = m_ViewportWorkSizeBackup * m_View.InvScale;
# else
viewport->WorkOffsetMin = m_ViewportWorkOffsetMinBackup * m_View.InvScale;
viewport->WorkOffsetMax = m_ViewportWorkOffsetMaxBackup * m_View.InvScale;
# endif
}
# endif
m_ViewRect = CalcViewRect(m_View);; m_ViewRect = CalcViewRect(m_View);;
auto& fringeScale = ImFringeScaleRef(m_DrawList); auto& fringeScale = ImFringeScaleRef(m_DrawList);
@@ -449,4 +522,5 @@ void ImGuiEx::Canvas::LeaveLocalSpace()
ImGui::PopClipRect(); ImGui::PopClipRect();
RestoreInputState(); RestoreInputState();
RestoreViewportState();
} }
+25
View File
@@ -210,6 +210,18 @@ private:
void SaveInputState(); void SaveInputState();
void RestoreInputState(); void RestoreInputState();
// UsdLayerManager local patch, ported from upstream imgui-node-editor:
// with a viewport-enabled ImGui (IMGUI_HAS_VIEWPORT, i.e. the docking
// branch), ImGui::IsMouseHoveringRect() — and therefore every item's
// hover test — requires the rect to overlap the mouse viewport's rect,
// which is in *screen* space. Inside the canvas, rects are in canvas
// space, so anything at canvas coordinates outside the screen-space
// range (e.g. negative) went hover/click-dead. Fix: transform the
// viewport rect into canvas-local space while inside EnterLocalSpace()
// and restore it on LeaveLocalSpace(), exactly like the mouse position.
void SaveViewportState();
void RestoreViewportState();
void EnterLocalSpace(); void EnterLocalSpace();
void LeaveLocalSpace(); void LeaveLocalSpace();
@@ -243,6 +255,19 @@ private:
ImVec2 m_MousePosPrevBackup; ImVec2 m_MousePosPrevBackup;
ImVec2 m_MouseClickedPosBackup[IM_ARRAYSIZE(ImGuiIO::MouseClickedPos)]; ImVec2 m_MouseClickedPosBackup[IM_ARRAYSIZE(ImGuiIO::MouseClickedPos)];
ImVec2 m_WindowCursorMaxBackup; ImVec2 m_WindowCursorMaxBackup;
# if defined(IMGUI_HAS_VIEWPORT)
ImVec2 m_WindowPosBackup;
ImVec2 m_ViewportPosBackup;
ImVec2 m_ViewportSizeBackup;
# if IMGUI_VERSION_NUM > 18002
ImVec2 m_ViewportWorkPosBackup;
ImVec2 m_ViewportWorkSizeBackup;
# else
ImVec2 m_ViewportWorkOffsetMinBackup;
ImVec2 m_ViewportWorkOffsetMaxBackup;
# endif
# endif
}; };
} // namespace ImGuiEx } // namespace ImGuiEx
+31 -2
View File
@@ -1,6 +1,5 @@
#include "NodeThumbnailCache.h" #include "NodeThumbnailCache.h"
#include "../utils/GLExt.h" #include "../utils/GLExt.h"
#include "../utils/Logger.h"
#include <pxr/imaging/hio/image.h> #include <pxr/imaging/hio/image.h>
#include <pxr/imaging/hio/types.h> #include <pxr/imaging/hio/types.h>
@@ -243,6 +242,32 @@ ImTextureID NodeThumbnailCache::GetMaterialThumbnail(const pxr::UsdStageRefPtr&
return e.texId; return e.texId;
} }
void NodeThumbnailCache::SyncRenderSettings(const pxr::TfToken& rendererPlugin, int ccMode,
const std::string& ocioDisplay, const std::string& ocioView,
const std::string& ocioColorSpace, const std::string& ocioLook) {
const bool changed = !m_renderSettingsInitialized ||
rendererPlugin != m_lastRendererPlugin || ccMode != m_lastCcMode ||
ocioDisplay != m_lastOcioDisplay || ocioView != m_lastOcioView ||
ocioColorSpace != m_lastOcioColorSpace || ocioLook != m_lastOcioLook;
if (!changed) return;
m_renderSettingsInitialized = true;
m_lastRendererPlugin = rendererPlugin;
m_lastCcMode = ccMode;
m_lastOcioDisplay = ocioDisplay;
m_lastOcioView = ocioView;
m_lastOcioColorSpace = ocioColorSpace;
m_lastOcioLook = ocioLook;
if (!rendererPlugin.IsEmpty())
m_matRenderer.SetRendererPlugin(rendererPlugin);
m_matRenderer.SetColorCorrection(ccMode, ocioDisplay, ocioView, ocioColorSpace, ocioLook);
// Every cached shader-ball thumbnail was rendered under the old settings.
for (auto& kv : m_matEntries)
kv.second.stale = true;
}
void NodeThumbnailCache::EnsureBlitFbos() { void NodeThumbnailCache::EnsureBlitFbos() {
if (!m_readFbo) glGenFramebuffers(1, &m_readFbo); if (!m_readFbo) glGenFramebuffers(1, &m_readFbo);
if (!m_drawFbo) glGenFramebuffers(1, &m_drawFbo); if (!m_drawFbo) glGenFramebuffers(1, &m_drawFbo);
@@ -317,7 +342,11 @@ void NodeThumbnailCache::PumpMainThread() {
const uint32_t src = m_matRenderer.Render(kThumbPx, kThumbPx); const uint32_t src = m_matRenderer.Render(kThumbPx, kThumbPx);
if (src != 0) { if (src != 0) {
BlitToEntry(e, src); BlitToEntry(e, src);
e.stale = false; // Progressive delegates (Arnold/Cycles/Embree) need several
// Render() calls to accumulate past their first noisy sample;
// keep re-pumping this entry until the image is final instead of
// caching whatever the first pass produced.
e.stale = !m_matRenderer.IsConverged();
} }
break; // one per frame break; // one per frame
} }
+20
View File
@@ -3,6 +3,7 @@
#include "MaterialPreviewRenderer.h" #include "MaterialPreviewRenderer.h"
#include <pxr/usd/usd/stage.h> #include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h> #include <pxr/usd/sdf/path.h>
#include <pxr/base/tf/token.h>
#include <imgui.h> #include <imgui.h>
#include <atomic> #include <atomic>
#include <condition_variable> #include <condition_variable>
@@ -53,6 +54,18 @@ public:
const std::string& output, bool terminal, const std::string& output, bool terminal,
size_t revision); size_t revision);
/// Keeps the material shader-ball thumbnails' render delegate and color
/// correction in sync with the main preview swatch's current settings
/// (they'd otherwise render under this cache's own independent Hydra
/// instance's defaults, mismatching the preview and the app's global
/// color-correction). Cheap to call every frame: no-ops unless something
/// actually changed, and only then invalidates cached thumbnails so
/// PumpMainThread re-renders them under the new settings. An empty
/// rendererPlugin means "not resolved yet" and is left alone.
void SyncRenderSettings(const pxr::TfToken& rendererPlugin, int ccMode,
const std::string& ocioDisplay, const std::string& ocioView,
const std::string& ocioColorSpace, const std::string& ocioLook);
/// Main thread, once per frame, GL context current: drains finished /// Main thread, once per frame, GL context current: drains finished
/// decodes and uploads them, renders at most one stale material thumbnail, /// decodes and uploads them, renders at most one stale material thumbnail,
/// and prunes entries not requested since the previous pump. /// and prunes entries not requested since the previous pump.
@@ -99,6 +112,13 @@ private:
unsigned int m_readFbo = 0; // scratch GL FBOs for the render->cache blit unsigned int m_readFbo = 0; // scratch GL FBOs for the render->cache blit
unsigned int m_drawFbo = 0; unsigned int m_drawFbo = 0;
// Last render settings applied via SyncRenderSettings, so repeated
// per-frame calls with unchanged values are cheap no-ops.
bool m_renderSettingsInitialized = false;
pxr::TfToken m_lastRendererPlugin;
int m_lastCcMode = -1;
std::string m_lastOcioDisplay, m_lastOcioView, m_lastOcioColorSpace, m_lastOcioLook;
void EnsureBlitFbos(); void EnsureBlitFbos();
void BlitToEntry(MatEntry& e, uint32_t srcTex); void BlitToEntry(MatEntry& e, uint32_t srcTex);
static void DeleteTex(ImTextureID& id); static void DeleteTex(ImTextureID& id);
+263
View File
@@ -0,0 +1,263 @@
#include "RenderLayerPanel.h"
#include "../core/commands/RenderLayerCommands.h"
#include "../core/commands/RenderLayerMembershipCommand.h"
#include <pxr/usd/usd/prim.h>
#include <imgui.h>
#include <cstring>
namespace UsdLayerManager {
RenderLayerPanel::RenderLayerPanel() {}
RenderLayerPanel::~RenderLayerPanel() {}
void RenderLayerPanel::SetStage(pxr::UsdStageRefPtr stage) {
m_stage = stage;
m_defaultSelected = true;
m_selectedLayerId.clear();
}
// ---------------------------------------------------------------------------
void RenderLayerPanel::Render() {
if (!m_renderLayerManager) {
ImGui::TextDisabled("No stage loaded");
return;
}
ImGui::Text("Render Layers");
ImGui::Separator();
ImGui::SetNextItemWidth(180.0f);
ImGui::InputText("##newLayerName", m_newLayerNameBuf, sizeof(m_newLayerNameBuf));
ImGui::SameLine();
if (ImGui::Button("Create Layer")) {
std::string name = m_newLayerNameBuf[0] ? m_newLayerNameBuf : "RenderLayer";
std::string newId;
if (m_commandHistory) {
auto cmd = std::make_unique<RenderLayerCreateCommand>(m_renderLayerManager, name);
RenderLayerCreateCommand* raw = cmd.get();
m_commandHistory->Push(std::move(cmd));
newId = raw->GetCreatedLayerId();
} else {
newId = m_renderLayerManager->CreateRenderLayer(name);
}
if (!newId.empty()) {
m_defaultSelected = false;
m_selectedLayerId = newId;
}
}
ImGui::Spacing();
RenderLayerList();
ImGui::Spacing();
ImGui::Separator();
ImGui::TextDisabled("Membership");
ImGui::Separator();
RenderMembershipPanel();
RenderRenameModal();
}
// ---------------------------------------------------------------------------
void RenderLayerPanel::RenderLayerList() {
auto layers = m_renderLayerManager->GetRenderLayers();
if (!ImGui::BeginTable("RenderLayerTable", 3,
ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY,
ImVec2(0, 150)))
return;
ImGui::TableSetupColumn("##active", ImGuiTableColumnFlags_WidthFixed, 24.0f);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##dirty", ImGuiTableColumnFlags_WidthFixed, 14.0f);
for (int i = 0; i < static_cast<int>(layers.size()); ++i) {
const auto& li = layers[i];
ImGui::TableNextRow();
ImGui::PushID(i);
// Col 0: activate radio button.
ImGui::TableSetColumnIndex(0);
{
bool isActive = li.isActive;
if (ImGui::RadioButton("##act", isActive) && !isActive) {
std::string target = li.isDefault ? std::string() : li.layerIdentifier;
if (m_commandHistory)
m_commandHistory->Push(
std::make_unique<RenderLayerActivateCommand>(m_renderLayerManager, target));
else if (li.isDefault)
m_renderLayerManager->ActivateDefaultLayer();
else
m_renderLayerManager->ActivateRenderLayer(target);
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip(isActive ? "Active" : "Click to activate");
}
// Col 1: name — selecting a row picks which layer's membership shows below.
ImGui::TableSetColumnIndex(1);
{
bool selected = li.isDefault
? m_defaultSelected
: (!m_defaultSelected && m_selectedLayerId == li.layerIdentifier);
ImVec4 nameColor = li.isActive ? ImVec4(0.5f, 1.0f, 0.5f, 1.0f)
: ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_Text, nameColor);
if (ImGui::Selectable(li.name.c_str(), selected,
ImGuiSelectableFlags_SpanAllColumns)) {
m_defaultSelected = li.isDefault;
m_selectedLayerId = li.isDefault ? std::string() : li.layerIdentifier;
}
ImGui::PopStyleColor();
if (!li.isDefault && ImGui::BeginPopupContextItem()) {
if (ImGui::MenuItem("Rename")) {
m_openRenameModal = true;
m_renameTargetId = li.layerIdentifier;
std::strncpy(m_renameBuf, li.name.c_str(), sizeof(m_renameBuf) - 1);
m_renameBuf[sizeof(m_renameBuf) - 1] = '\0';
}
if (ImGui::MenuItem("Delete")) {
if (m_commandHistory)
m_commandHistory->Push(
std::make_unique<RenderLayerDeleteCommand>(m_renderLayerManager, li.layerIdentifier));
else
m_renderLayerManager->DeleteRenderLayer(li.layerIdentifier);
if (!m_defaultSelected && m_selectedLayerId == li.layerIdentifier) {
m_defaultSelected = true;
m_selectedLayerId.clear();
}
}
ImGui::EndPopup();
}
}
// Col 2: dirty indicator.
ImGui::TableSetColumnIndex(2);
if (!li.isDefault && li.layer && li.layer->IsDirty())
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "*");
else
ImGui::TextDisabled(" ");
ImGui::PopID();
}
ImGui::EndTable();
}
// ---------------------------------------------------------------------------
void RenderLayerPanel::RenderMembershipPanel() {
if (m_defaultSelected) {
ImGui::TextDisabled("Default has no membership of its own - it always shows the full, unmodified stage.");
return;
}
if (m_selectedLayerId.empty()) {
ImGui::TextDisabled("Select a render layer above to edit its membership.");
return;
}
std::string displayName = m_selectedLayerId;
for (const auto& li : m_renderLayerManager->GetRenderLayers())
if (!li.isDefault && li.layerIdentifier == m_selectedLayerId) { displayName = li.name; break; }
ImGui::Text("Members of \"%s\"", displayName.c_str());
auto members = m_renderLayerManager->GetMembers(m_selectedLayerId);
auto isValid = [&](const pxr::SdfPath& p) {
return m_stage && m_stage->GetPrimAtPath(p).IsValid();
};
bool anyMissing = false;
for (const auto& p : members)
if (!isValid(p)) { anyMissing = true; break; }
if (m_sceneHierarchyPanel && ImGui::Button("Add Selected")) {
auto paths = m_sceneHierarchyPanel->GetSelectedPaths();
if (!paths.empty()) {
std::vector<pxr::SdfPath> sdfPaths;
sdfPaths.reserve(paths.size());
for (const auto& p : paths) sdfPaths.emplace_back(p);
if (m_commandHistory)
m_commandHistory->Push(std::make_unique<RenderLayerAddMembersCommand>(
m_renderLayerManager, m_selectedLayerId, sdfPaths));
else
for (const auto& sp : sdfPaths) m_renderLayerManager->AddMember(m_selectedLayerId, sp);
}
}
if (anyMissing) {
ImGui::SameLine();
if (ImGui::Button("Remove Missing")) {
std::vector<pxr::SdfPath> toRemove;
for (const auto& p : members)
if (!isValid(p)) toRemove.push_back(p);
if (m_commandHistory)
m_commandHistory->Push(std::make_unique<RenderLayerRemoveMembersCommand>(
m_renderLayerManager, m_selectedLayerId, toRemove));
else
for (const auto& p : toRemove) m_renderLayerManager->RemoveMember(m_selectedLayerId, p);
}
}
if (members.empty()) {
ImGui::TextDisabled("No members. Select lights/objects in Scene Hierarchy and click \"Add Selected\".");
return;
}
if (ImGui::BeginChild("RenderLayerMembersList", ImVec2(0, 0), true)) {
for (const auto& path : members) {
ImGui::PushID(path.GetString().c_str());
bool missing = !isValid(path);
if (missing) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.45f, 1.0f));
ImGui::TextUnformatted(path.GetText());
if (missing) {
ImGui::PopStyleColor();
ImGui::SameLine();
ImGui::TextDisabled("(missing)");
}
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - 24.0f);
if (ImGui::SmallButton("x")) {
if (m_commandHistory)
m_commandHistory->Push(std::make_unique<RenderLayerRemoveMembersCommand>(
m_renderLayerManager, m_selectedLayerId, std::vector<pxr::SdfPath>{path}));
else
m_renderLayerManager->RemoveMember(m_selectedLayerId, path);
}
ImGui::PopID();
}
}
ImGui::EndChild();
}
// ---------------------------------------------------------------------------
void RenderLayerPanel::RenderRenameModal() {
if (m_openRenameModal) {
ImGui::OpenPopup("Rename Render Layer");
m_openRenameModal = false;
}
if (ImGui::BeginPopupModal("Rename Render Layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::SetNextItemWidth(240.0f);
bool enterPressed = ImGui::InputText("Name", m_renameBuf, sizeof(m_renameBuf),
ImGuiInputTextFlags_EnterReturnsTrue);
bool okClicked = ImGui::Button("OK");
ImGui::SameLine();
bool cancelClicked = ImGui::Button("Cancel");
if ((enterPressed || okClicked) && m_renameBuf[0]) {
if (m_commandHistory)
m_commandHistory->Push(std::make_unique<RenderLayerRenameCommand>(
m_renderLayerManager, m_renameTargetId, std::string(m_renameBuf)));
else
m_renderLayerManager->RenameRenderLayer(m_renameTargetId, m_renameBuf);
ImGui::CloseCurrentPopup();
} else if (cancelClicked) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
} // namespace UsdLayerManager
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include "../core/RenderLayerManager.h"
#include "../core/CommandHistory.h"
#include "SceneHierarchyPanel.h"
#include "IconManager.h"
#include <pxr/usd/usd/stage.h>
#include <string>
namespace UsdLayerManager {
/// Maya-Render-Layers-style panel: layer list with one-click activation,
/// create/rename/delete, and a membership editor for whichever layer is
/// selected (independent of which one is active — you can inspect/edit a
/// layer's members without switching to it).
class RenderLayerPanel {
public:
RenderLayerPanel();
~RenderLayerPanel();
void SetRenderLayerManager(RenderLayerManager* mgr) { m_renderLayerManager = mgr; }
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
/// Optional — powers the "Add Selected" membership button.
void SetSceneHierarchyPanel(SceneHierarchyPanel* panel) { m_sceneHierarchyPanel = panel; }
void SetIconManager(IconManager* icons) { m_iconManager = icons; }
void SetStage(pxr::UsdStageRefPtr stage);
void Render();
private:
void RenderLayerList();
void RenderMembershipPanel();
void RenderRenameModal();
RenderLayerManager* m_renderLayerManager = nullptr;
CommandHistory* m_commandHistory = nullptr;
SceneHierarchyPanel* m_sceneHierarchyPanel = nullptr;
IconManager* m_iconManager = nullptr;
pxr::UsdStageRefPtr m_stage;
/// Which layer's membership the lower panel shows — independent of which
/// layer is currently active.
bool m_defaultSelected = true;
std::string m_selectedLayerId;
char m_newLayerNameBuf[128] = "RenderLayer";
bool m_openRenameModal = false;
std::string m_renameTargetId;
char m_renameBuf[128] = {};
};
} // namespace UsdLayerManager
+276
View File
@@ -8,6 +8,7 @@
#include "../core/commands/RenamePrimCommand.h" #include "../core/commands/RenamePrimCommand.h"
#include "../core/commands/ReparentPrimCommand.h" #include "../core/commands/ReparentPrimCommand.h"
#include "../core/commands/GroupPrimsCommand.h" #include "../core/commands/GroupPrimsCommand.h"
#include "../core/commands/LayerCommands.h"
#include <pxr/usd/usd/prim.h> #include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/primRange.h> #include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usd/references.h> #include <pxr/usd/usd/references.h>
@@ -309,6 +310,8 @@ void SceneHierarchyPanel::Render() {
} }
} }
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
ImGui::BeginDisabled(renderLayerActive);
ImGui::SetNextItemWidth(-1.0f); ImGui::SetNextItemWidth(-1.0f);
if (ImGui::BeginCombo("##layerswitch", currentLabel.c_str(), if (ImGui::BeginCombo("##layerswitch", currentLabel.c_str(),
ImGuiComboFlags_HeightRegular)) { ImGuiComboFlags_HeightRegular)) {
@@ -326,9 +329,16 @@ void SceneHierarchyPanel::Render() {
} }
ImGui::EndCombo(); ImGui::EndCombo();
} }
bool comboHovered = ImGui::IsItemHovered();
ImGui::EndDisabled();
if (renderLayerActive && comboHovered)
ImGui::SetTooltip("Edit target is locked to the active render layer");
ImGui::Spacing(); ImGui::Spacing();
} }
RenderSublayerSection();
ImGui::Spacing();
auto paths = m_propertyManager->GetPrimPaths(); auto paths = m_propertyManager->GetPrimPaths();
if (paths.empty()) { if (paths.empty()) {
ImGui::TextDisabled("No prims in stage"); ImGui::TextDisabled("No prims in stage");
@@ -582,6 +592,272 @@ void SceneHierarchyPanel::Render() {
ProcessPendingReplaceRef(); ProcessPendingReplaceRef();
} }
// ---------------------------------------------------------------------------
void SceneHierarchyPanel::RenderSublayerSection() {
if (!ImGui::CollapsingHeader("Sublayers", ImGuiTreeNodeFlags_DefaultOpen))
return;
if (!m_layerManager) {
ImGui::TextDisabled(" No layer manager");
return;
}
auto sublayers = m_layerManager->GetSublayers();
if (sublayers.empty()) {
ImGui::TextDisabled(" No sublayers");
return;
}
const float rowHeight = ImGui::GetTextLineHeightWithSpacing();
const int visibleRows = std::min<int>(static_cast<int>(sublayers.size()), 6);
float tableHeight = rowHeight * (visibleRows + 1); // +1 for header row
if (!ImGui::BeginTable("HierarchySublayerTable", 6,
ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY,
ImVec2(0, tableHeight)))
return;
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("##drag", ImGuiTableColumnFlags_WidthFixed, 14.0f);
ImGui::TableSetupColumn("##dirty", ImGuiTableColumnFlags_WidthFixed, 14.0f);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##et", ImGuiTableColumnFlags_WidthFixed, 28.0f);
ImGui::TableSetupColumn("Muted", ImGuiTableColumnFlags_WidthFixed, 55.0f);
ImGui::TableSetupColumn("##order", ImGuiTableColumnFlags_WidthFixed, 44.0f);
// Custom header row: pen icon for the ET column, text for the rest.
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
ImGui::TableSetColumnIndex(2); ImGui::TableHeader("Name");
ImGui::TableSetColumnIndex(3);
{
float cellW = ImGui::GetContentRegionAvail().x;
float iconW = 14.0f;
float off = (cellW - iconW) * 0.5f;
if (off > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + off);
if (m_iconManager)
ImGui::Image(ImTextureRef(m_iconManager->Get(Icon::Pen)), ImVec2(iconW, iconW));
else
ImGui::TextUnformatted("ET");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Edit Target");
}
ImGui::TableSetColumnIndex(4); ImGui::TableHeader("Muted");
ImGui::TableSetColumnIndex(5); ImGui::TableHeader("Order");
for (int i = 0; i < static_cast<int>(sublayers.size()); i++) {
const auto& li = sublayers[i];
ImGui::TableNextRow();
ImGui::PushID(i);
// Col 0: invisible span-all Selectable for row selection / drag / context menu.
ImGui::TableSetColumnIndex(0);
ImVec2 rowStart = ImGui::GetCursorScreenPos(); // save before Selectable moves cursor
bool isSelected = (m_sublayerSelectedIdx == i);
if (ImGui::Selectable("##row", isSelected,
ImGuiSelectableFlags_SpanAllColumns |
ImGuiSelectableFlags_AllowOverlap,
ImVec2(0, 0)))
m_sublayerSelectedIdx = i;
// Context menu: must come immediately after the span-all Selectable so
// BeginPopupContextItem sees it as the "last item" and responds to
// right-click anywhere in the row.
RenderSublayerContextMenu(i, sublayers);
// Drag source: also attached to the Selectable (left-button drag).
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
ImGui::SetDragDropPayload("HIER_SUBLAYER_IDX", &i, sizeof(int));
ImGui::Text("%s", li.displayName.c_str());
ImGui::EndDragDropSource();
}
// Drop target on the Selectable.
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload =
ImGui::AcceptDragDropPayload("HIER_SUBLAYER_IDX")) {
int src = *static_cast<const int*>(payload->Data);
int dst = i;
if (src != dst) {
std::vector<std::string> before, after;
for (const auto& sl : sublayers) before.push_back(sl.identifier);
after = before;
std::string moved = after[src];
after.erase(after.begin() + src);
after.insert(after.begin() + dst, moved);
if (m_commandHistory) {
m_commandHistory->Push(std::make_unique<LayerReorderCommand>(
m_layerManager, before, after));
} else {
LayerReorderCommand cmd(m_layerManager, before, after);
static_cast<ICommand&>(cmd).Execute();
}
}
}
ImGui::EndDragDropTarget();
}
// "=" drag handle: overlay at col 0 using the saved screen position so it
// sits inside the row, not below it (TableSetColumnIndex only resets X).
ImGui::SetCursorScreenPos(rowStart);
ImGui::TextDisabled("=");
// Col 1: dirty indicator
ImGui::TableSetColumnIndex(1);
if (li.isDirty)
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "*");
else
ImGui::TextDisabled(" ");
bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
// Col 2: layer name
ImGui::TableSetColumnIndex(2);
ImVec4 nameColor = li.isMuted
? ImVec4(0.5f, 0.5f, 0.5f, 1.0f)
: ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
ImGui::TextColored(nameColor, "%s", li.displayName.c_str());
if (ImGui::IsItemHovered() && !li.realPath.empty())
ImGui::SetTooltip("%s\n%s", li.identifier.c_str(), li.realPath.c_str());
if (isRenderLayer) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.9f, 0.7f, 0.2f, 1.0f), "[RL]");
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Managed from the Render Layer panel");
}
// Col 3: edit target checkbox — checking sets this layer as edit target;
// unchecking does nothing (there is always an active edit target).
ImGui::TableSetColumnIndex(3);
{
bool isET = li.isEditTarget;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
ImGui::BeginDisabled(renderLayerActive);
if (ImGui::Checkbox("##et", &isET) && isET)
m_layerManager->SetEditTarget(li.identifier);
ImGui::EndDisabled();
ImGui::PopStyleVar();
if (ImGui::IsItemHovered())
ImGui::SetTooltip(renderLayerActive ? "Edit target is locked to the active render layer"
: li.isEditTarget ? "Edit target (active)"
: "Set as edit target");
}
// Col 4: mute checkbox — disabled for render-layer sublayers, which
// RenderLayerManager mutes/unmutes exclusively as layers are switched.
ImGui::TableSetColumnIndex(4);
{
bool muted = li.isMuted;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
ImGui::BeginDisabled(isRenderLayer);
if (ImGui::Checkbox("##muted", &muted)) {
if (muted) m_layerManager->MuteLayer(li.identifier);
else m_layerManager->UnmuteLayer(li.identifier);
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
if (isRenderLayer && ImGui::IsItemHovered())
ImGui::SetTooltip("Managed from the Render Layer panel");
}
// Col 5: up/down reorder icon buttons.
ImGui::TableSetColumnIndex(5);
{
bool canUp = (i > 0);
bool canDown = (i < static_cast<int>(sublayers.size()) - 1);
auto buildBeforeAfter = [&](int a, int b,
std::vector<std::string>& before,
std::vector<std::string>& after) {
for (const auto& sl : sublayers) before.push_back(sl.identifier);
after = before;
std::swap(after[a], after[b]);
};
ImVec2 iconSize(14.0f, 14.0f);
ImGui::BeginDisabled(!canUp);
ImTextureID upTex = m_iconManager ? m_iconManager->Get(Icon::ChevronUp) : ImTextureID_Invalid;
if (m_iconManager ? ImGui::ImageButton("##up", ImTextureRef(upTex), iconSize)
: ImGui::ArrowButton("##up", ImGuiDir_Up)) {
std::vector<std::string> before, after;
buildBeforeAfter(i, i - 1, before, after);
if (m_commandHistory)
m_commandHistory->Push(
std::make_unique<LayerReorderCommand>(m_layerManager, before, after));
else {
LayerReorderCommand cmd(m_layerManager, before, after);
static_cast<ICommand&>(cmd).Execute();
}
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Move Up");
ImGui::SameLine(0.0f, 2.0f);
ImGui::BeginDisabled(!canDown);
ImTextureID downTex = m_iconManager ? m_iconManager->Get(Icon::ChevronDown) : ImTextureID_Invalid;
if (m_iconManager ? ImGui::ImageButton("##down", ImTextureRef(downTex), iconSize)
: ImGui::ArrowButton("##down", ImGuiDir_Down)) {
std::vector<std::string> before, after;
buildBeforeAfter(i, i + 1, before, after);
if (m_commandHistory)
m_commandHistory->Push(
std::make_unique<LayerReorderCommand>(m_layerManager, before, after));
else {
LayerReorderCommand cmd(m_layerManager, before, after);
static_cast<ICommand&>(cmd).Execute();
}
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Move Down");
}
ImGui::PopID();
}
ImGui::EndTable();
}
// ---------------------------------------------------------------------------
void SceneHierarchyPanel::RenderSublayerContextMenu(int i,
const std::vector<LayerInfo>& sublayers) {
if (!ImGui::BeginPopupContextItem(("ctx_sublayer_" + std::to_string(i)).c_str()))
return;
const auto& li = sublayers[i];
bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
if (ImGui::MenuItem(li.isMuted ? "Unmute" : "Mute", nullptr, false, !isRenderLayer)) {
if (li.isMuted) m_layerManager->UnmuteLayer(li.identifier);
else m_layerManager->MuteLayer(li.identifier);
}
if (isRenderLayer && ImGui::IsItemHovered())
ImGui::SetTooltip("Managed from the Render Layer panel");
if (!li.isEditTarget &&
ImGui::MenuItem("Set as Edit Target", nullptr, false, !renderLayerActive))
m_layerManager->SetEditTarget(li.identifier);
if (renderLayerActive && !li.isEditTarget && ImGui::IsItemHovered())
ImGui::SetTooltip("Edit target is locked to the active render layer");
ImGui::Separator();
if (ImGui::MenuItem("Remove", nullptr, false, !isRenderLayer)) {
if (m_commandHistory)
m_commandHistory->Push(
std::make_unique<LayerRemoveCommand>(m_layerManager, i));
else
m_layerManager->RemoveSublayer(i);
}
if (isRenderLayer && ImGui::IsItemHovered())
ImGui::SetTooltip("Managed from the Render Layer panel");
ImGui::EndPopup();
}
void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) { void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
if (!prim.IsValid()) return; if (!prim.IsValid()) return;
+18
View File
@@ -3,6 +3,7 @@
#include "../core/PropertyManager.h" #include "../core/PropertyManager.h"
#include "../core/CommandHistory.h" #include "../core/CommandHistory.h"
#include "../core/LayerManager.h" #include "../core/LayerManager.h"
#include "../core/RenderLayerManager.h"
#include "IconManager.h" #include "IconManager.h"
#include <pxr/usd/usd/stage.h> #include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h> #include <pxr/usd/sdf/path.h>
@@ -26,6 +27,11 @@ public:
void SetPropertyManager(PropertyManager* manager); void SetPropertyManager(PropertyManager* manager);
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; } void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
void SetLayerManager(LayerManager* lm) { m_layerManager = lm; } void SetLayerManager(LayerManager* lm) { m_layerManager = lm; }
/// Optional. When set and a non-Default render layer is active, the
/// edit-target layer switcher below is disabled — repointing the ambient
/// edit target away from the active render layer's SdfLayer would break
/// the invariant that edits made while it's active land in that layer.
void SetRenderLayerManager(RenderLayerManager* mgr) { m_renderLayerManager = mgr; }
void SetStage(UsdStageRefPtr stage); void SetStage(UsdStageRefPtr stage);
void SetIconManager(IconManager* iconManager) { m_iconManager = iconManager; } void SetIconManager(IconManager* iconManager) { m_iconManager = iconManager; }
void Render(); void Render();
@@ -33,6 +39,12 @@ public:
std::string GetSelectedPrimPath() const { return m_primarySelectedPath; } std::string GetSelectedPrimPath() const { return m_primarySelectedPath; }
UsdPrim GetSelectedPrim() const; UsdPrim GetSelectedPrim() const;
/// Full multi-selection (rect pick or Ctrl/Shift click), in no
/// particular order. Empty when nothing is selected.
std::vector<std::string> GetSelectedPaths() const {
return std::vector<std::string>(m_selectedPaths.begin(), m_selectedPaths.end());
}
/// Set single selection from hierarchy click (fires callback). /// Set single selection from hierarchy click (fires callback).
/// Also clears any rect multi-selection. /// Also clears any rect multi-selection.
void SetSelectedPathFromClick(const std::string& path); void SetSelectedPathFromClick(const std::string& path);
@@ -79,10 +91,13 @@ private:
void RenderRemovePrimModal(); void RenderRemovePrimModal();
void RenderArcModal(); void RenderArcModal();
void ProcessPendingReplaceRef(); void ProcessPendingReplaceRef();
void RenderSublayerSection();
void RenderSublayerContextMenu(int i, const std::vector<LayerInfo>& sublayers);
PropertyManager* m_propertyManager; PropertyManager* m_propertyManager;
CommandHistory* m_commandHistory = nullptr; CommandHistory* m_commandHistory = nullptr;
LayerManager* m_layerManager = nullptr; LayerManager* m_layerManager = nullptr;
RenderLayerManager* m_renderLayerManager = nullptr;
IconManager* m_iconManager = nullptr; IconManager* m_iconManager = nullptr;
UsdStageRefPtr m_stage; UsdStageRefPtr m_stage;
@@ -94,6 +109,9 @@ private:
bool m_scrollToSelected = false; bool m_scrollToSelected = false;
/// Selected row in the Sublayers section (context menu / drag anchor).
int m_sublayerSelectedIdx = -1;
/// Stage-local layer identifiers rebuilt once per Render() for override detection. /// Stage-local layer identifiers rebuilt once per Render() for override detection.
/// Contains only the stage's own layers (root + sublayers + session), /// Contains only the stage's own layers (root + sublayers + session),
/// NOT layers that came in through references or payloads. /// NOT layers that came in through references or payloads.
+38 -6
View File
@@ -123,13 +123,18 @@ void StageEditorPanel::RenderFixedLayers(const std::vector<LayerInfo>& layers) {
ImGui::TableSetColumnIndex(3); ImGui::TableSetColumnIndex(3);
{ {
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
bool isET = li.isEditTarget; bool isET = li.isEditTarget;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
ImGui::BeginDisabled(renderLayerActive);
if (ImGui::Checkbox("##et", &isET) && isET) if (ImGui::Checkbox("##et", &isET) && isET)
m_layerManager->SetEditTarget(li.identifier); m_layerManager->SetEditTarget(li.identifier);
ImGui::EndDisabled();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip(li.isEditTarget ? "Edit target (active)" : "Set as edit target"); ImGui::SetTooltip(renderLayerActive ? "Edit target is locked to the active render layer"
: li.isEditTarget ? "Edit target (active)"
: "Set as edit target");
} }
ImGui::PopID(); ImGui::PopID();
@@ -242,6 +247,9 @@ void StageEditorPanel::RenderSublayerList(const std::vector<LayerInfo>& sublayer
else else
ImGui::TextDisabled(" "); ImGui::TextDisabled(" ");
bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
// Col 2: layer name // Col 2: layer name
ImGui::TableSetColumnIndex(2); ImGui::TableSetColumnIndex(2);
ImVec4 nameColor = li.isMuted ImVec4 nameColor = li.isMuted
@@ -250,6 +258,12 @@ void StageEditorPanel::RenderSublayerList(const std::vector<LayerInfo>& sublayer
ImGui::TextColored(nameColor, "%s", li.displayName.c_str()); ImGui::TextColored(nameColor, "%s", li.displayName.c_str());
if (ImGui::IsItemHovered() && !li.realPath.empty()) if (ImGui::IsItemHovered() && !li.realPath.empty())
ImGui::SetTooltip("%s\n%s", li.identifier.c_str(), li.realPath.c_str()); ImGui::SetTooltip("%s\n%s", li.identifier.c_str(), li.realPath.c_str());
if (isRenderLayer) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.9f, 0.7f, 0.2f, 1.0f), "[RL]");
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Managed from the Render Layer panel");
}
// Col 3: edit target checkbox — checking sets this layer as edit target; // Col 3: edit target checkbox — checking sets this layer as edit target;
// unchecking does nothing (there is always an active edit target). // unchecking does nothing (there is always an active edit target).
@@ -257,23 +271,32 @@ void StageEditorPanel::RenderSublayerList(const std::vector<LayerInfo>& sublayer
{ {
bool isET = li.isEditTarget; bool isET = li.isEditTarget;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
ImGui::BeginDisabled(renderLayerActive);
if (ImGui::Checkbox("##et", &isET) && isET) if (ImGui::Checkbox("##et", &isET) && isET)
m_layerManager->SetEditTarget(li.identifier); m_layerManager->SetEditTarget(li.identifier);
ImGui::EndDisabled();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip(li.isEditTarget ? "Edit target (active)" : "Set as edit target"); ImGui::SetTooltip(renderLayerActive ? "Edit target is locked to the active render layer"
: li.isEditTarget ? "Edit target (active)"
: "Set as edit target");
} }
// Col 4: mute checkbox // Col 4: mute checkbox — disabled for render-layer sublayers, which
// RenderLayerManager mutes/unmutes exclusively as layers are switched.
ImGui::TableSetColumnIndex(4); ImGui::TableSetColumnIndex(4);
{ {
bool muted = li.isMuted; bool muted = li.isMuted;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
ImGui::BeginDisabled(isRenderLayer);
if (ImGui::Checkbox("##muted", &muted)) { if (ImGui::Checkbox("##muted", &muted)) {
if (muted) m_layerManager->MuteLayer(li.identifier); if (muted) m_layerManager->MuteLayer(li.identifier);
else m_layerManager->UnmuteLayer(li.identifier); else m_layerManager->UnmuteLayer(li.identifier);
} }
ImGui::EndDisabled();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
if (isRenderLayer && ImGui::IsItemHovered())
ImGui::SetTooltip("Managed from the Render Layer panel");
} }
ImGui::PopID(); ImGui::PopID();
@@ -289,14 +312,21 @@ void StageEditorPanel::RenderSublayerContextMenu(int i,
return; return;
const auto& li = sublayers[i]; const auto& li = sublayers[i];
bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
if (ImGui::MenuItem(li.isMuted ? "Unmute" : "Mute")) { if (ImGui::MenuItem(li.isMuted ? "Unmute" : "Mute", nullptr, false, !isRenderLayer)) {
if (li.isMuted) m_layerManager->UnmuteLayer(li.identifier); if (li.isMuted) m_layerManager->UnmuteLayer(li.identifier);
else m_layerManager->MuteLayer(li.identifier); else m_layerManager->MuteLayer(li.identifier);
} }
if (isRenderLayer && ImGui::IsItemHovered())
ImGui::SetTooltip("Managed from the Render Layer panel");
if (!li.isEditTarget && ImGui::MenuItem("Set as Edit Target")) if (!li.isEditTarget &&
ImGui::MenuItem("Set as Edit Target", nullptr, false, !renderLayerActive))
m_layerManager->SetEditTarget(li.identifier); m_layerManager->SetEditTarget(li.identifier);
if (renderLayerActive && !li.isEditTarget && ImGui::IsItemHovered())
ImGui::SetTooltip("Edit target is locked to the active render layer");
ImGui::Separator(); ImGui::Separator();
@@ -337,13 +367,15 @@ void StageEditorPanel::RenderSublayerContextMenu(int i,
ImGui::Separator(); ImGui::Separator();
if (ImGui::MenuItem("Remove")) { if (ImGui::MenuItem("Remove", nullptr, false, !isRenderLayer)) {
if (m_commandHistory) if (m_commandHistory)
m_commandHistory->Push( m_commandHistory->Push(
std::make_unique<LayerRemoveCommand>(m_layerManager, i)); std::make_unique<LayerRemoveCommand>(m_layerManager, i));
else else
m_layerManager->RemoveSublayer(i); m_layerManager->RemoveSublayer(i);
} }
if (isRenderLayer && ImGui::IsItemHovered())
ImGui::SetTooltip("Managed from the Render Layer panel");
ImGui::EndPopup(); ImGui::EndPopup();
} }
+8
View File
@@ -2,6 +2,7 @@
#include "../core/LayerManager.h" #include "../core/LayerManager.h"
#include "../core/CommandHistory.h" #include "../core/CommandHistory.h"
#include "../core/RenderLayerManager.h"
#include "IconManager.h" #include "IconManager.h"
#include <imgui.h> #include <imgui.h>
#include <memory> #include <memory>
@@ -17,6 +18,12 @@ public:
void SetLayerManager(LayerManager* manager) { m_layerManager = manager; } void SetLayerManager(LayerManager* manager) { m_layerManager = manager; }
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; } void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
void SetIconManager(IconManager* icons) { m_iconManager = icons; } void SetIconManager(IconManager* icons) { m_iconManager = icons; }
/// Optional. When set: sublayers RenderLayerManager created are badged
/// and have their mute/remove controls disabled here (they're managed
/// exclusively from the Render Layer panel so the two can't desync), and
/// every row's edit-target checkbox is disabled while a non-Default
/// render layer is active.
void SetRenderLayerManager(RenderLayerManager* mgr) { m_renderLayerManager = mgr; }
void Render(); void Render();
private: private:
@@ -27,6 +34,7 @@ private:
LayerManager* m_layerManager = nullptr; LayerManager* m_layerManager = nullptr;
CommandHistory* m_commandHistory = nullptr; CommandHistory* m_commandHistory = nullptr;
IconManager* m_iconManager = nullptr; IconManager* m_iconManager = nullptr;
RenderLayerManager* m_renderLayerManager = nullptr;
int m_selectedIdx = -1; int m_selectedIdx = -1;
}; };
+5 -1
View File
@@ -32,7 +32,11 @@ struct ViewportTileSettings {
bool ambientLightOnly = true; bool ambientLightOnly = true;
bool domeLightEnabled = false; bool domeLightEnabled = false;
int shadingMode = 0; int shadingMode = 0;
int cullStyle = 4; // CullStyle::BackUnlessDoubleSided // CullStyle::Nothing — matches usdview's default (viewSettingsDataModel
// cullBackfaces=False -> CULL_STYLE_NOTHING). BackUnlessDoubleSided drops
// back faces on geometry that isn't authored doubleSided, which hdEmbree
// applies to occlusion rays as well, so interiors shade as single-sided.
int cullStyle = 1; // CullStyle::Nothing
bool showGuides = false; bool showGuides = false;
bool showProxy = true; bool showProxy = true;
bool showRender = false; bool showRender = false;