Compare commits

...

10 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
46 changed files with 2752 additions and 166 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
``` ```
+75 -45
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
add_custom_command(TARGET UsdLayerManager POST_BUILD # (e.g. OpenColorIO_2_2.dll) and changes whenever the USD build's bundled
COMMAND ${CMAKE_COMMAND} -E copy_if_different # OCIO version changes, so glob for it instead of hardcoding the version.
"${OpenUSD_ROOT_DIR}/bin/OpenColorIO_2_1.dll" file(GLOB OPENCOLORIO_RUNTIME_DLL "${OpenUSD_ROOT_DIR}/bin/OpenColorIO_*.dll")
"$<TARGET_FILE_DIR:UsdLayerManager>" if(OPENCOLORIO_RUNTIME_DLL)
COMMENT "Copying OpenColorIO runtime DLL..." add_custom_command(TARGET UsdLayerManager POST_BUILD
) COMMAND ${CMAKE_COMMAND} -E copy_if_different
${OPENCOLORIO_RUNTIME_DLL}
"$<TARGET_FILE_DIR:UsdLayerManager>"
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"
} }
}, },
+54 -35
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,31 +80,46 @@ 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)
│ ├── AttributeSetCommand — Type-agnostic undoable attribute writes │ ├── RenamePrimCommand — Undoable prim rename
│ ├── AddReferenceCommand — Undoable reference addition │ ├── ReparentPrimCommand — Undoable reparent via UsdNamespaceEditor
│ ├── ReplaceReferenceCommand — Undoable reference replacement │ ├── GroupPrimsCommand — Group selected prims under a new Xform
── LayerCommands Create, remove, reorder sublayers ── AttributeSetCommand — Type-agnostic undoable attribute writes
│ ├── AddReferenceCommand — Undoable reference addition
│ ├── 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
├── 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)
│ ├── ImGuiContext — Win32+OpenGL+ImGui initialization and loop │ ├── ImGuiContext — Win32+OpenGL+ImGui initialization and loop
│ ├── ViewportPanel — Viewport container (layout, divider drag, maximize) │ ├── ViewportPanel — Viewport container (layout, divider drag, maximize)
│ ├── 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)
│ ├── TransformManipulator — ImDrawList gizmo (move/rotate/scale) │ ├── TimelinePanel — Transport controls, loop/bounce, frame scrub, Playblast
── IconManager — SVG icon rasterization via NanoSVG ── 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)
│ ├── 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

+6 -2
View File
@@ -195,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))
+4
View File
@@ -31,6 +31,10 @@ 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. /// Hypershade-style pin display mode for a node in the graph canvas.
+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)
+29 -10
View File
@@ -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;
} }
pxr::UsdShadeInput destInput = destShader.CreateInput(pxr::TfToken(m_destInput), m_destType); if (m_editLayer) {
pxr::UsdShadeOutput sourceOutput = sourceShader.CreateOutput(pxr::TfToken(m_sourceOutput), m_sourceType); pxr::UsdEditContext ec(m_stage, m_editLayer);
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, sourceOutput); 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);
} 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,13 +76,22 @@ 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;
if (m_hadPriorConnection) { auto doUndo = [&]() {
pxr::UsdShadeShader priorSourceShader(m_stage->GetPrimAtPath(m_priorSourceNode)); if (m_hadPriorConnection) {
pxr::UsdShadeOutput priorSourceOutput = priorSourceShader.GetOutput(pxr::TfToken(m_priorSourceOutput)); pxr::UsdShadeShader priorSourceShader(m_stage->GetPrimAtPath(m_priorSourceNode));
if (priorSourceShader && priorSourceOutput) pxr::UsdShadeOutput priorSourceOutput = priorSourceShader.GetOutput(pxr::TfToken(m_priorSourceOutput));
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, priorSourceOutput); if (priorSourceShader && priorSourceOutput)
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, priorSourceOutput);
} else {
pxr::UsdShadeConnectableAPI::DisconnectSource(destInput);
}
};
if (m_editLayer) {
pxr::UsdEditContext ec(m_stage, m_editLayer);
doUndo();
} else { } else {
pxr::UsdShadeConnectableAPI::DisconnectSource(destInput); 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;
pxr::UsdShadeConnectableAPI::DisconnectSource(input);
if (m_editLayer) {
pxr::UsdEditContext ec(m_stage, m_editLayer);
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 + "\\*";
+128
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);
} }
@@ -440,6 +481,76 @@ void Application::LoadPreferences()
} }
} }
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()
{ {
// Pull the latest splitter positions; called from Shutdown before the // Pull the latest splitter positions; called from Shutdown before the
@@ -765,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);
@@ -860,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() {
@@ -879,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
@@ -888,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.
+15
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"
@@ -65,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();
@@ -78,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;
@@ -91,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
+240 -37
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;
@@ -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,11 +667,39 @@ 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();
char buf[512];
std::snprintf(buf, sizeof(buf), "%s", s.c_str()); if (!input.allowedValues.empty()) {
if (ImGui::InputText(widgetId.c_str(), buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) { // Sdr-declared enum (e.g. UsdUVTexture's sourceColorSpace:
if (type == tn->String) newValue = std::string(buf); // raw/sRGB/auto) — a dropdown of exactly the legal values instead
else newValue = pxr::TfToken(buf); // 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];
std::snprintf(buf, sizeof(buf), "%s", s.c_str());
if (ImGui::InputText(widgetId.c_str(), buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) {
if (type == tn->String) newValue = std::string(buf);
else newValue = pxr::TfToken(buf);
}
} }
} else if (type == tn->Asset) { } else if (type == tn->Asset) {
std::string s; std::string s;
@@ -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;
pxr::UsdShadeMaterialBindingAPI bindAPI(target); auto doUnbind = [&]() {
if (hadPriorRel && !priorTargets.empty()) pxr::UsdShadeMaterialBindingAPI bindAPI(target);
bindAPI.GetDirectBindingRel().SetTargets(priorTargets); if (hadPriorRel && !priorTargets.empty())
else bindAPI.GetDirectBindingRel().SetTargets(priorTargets);
bindAPI.UnbindDirectBinding(); else
bindAPI.UnbindDirectBinding();
};
if (editLayer) {
pxr::UsdEditContext ec(stage, editLayer);
doUnbind();
} else {
doUnbind();
}
} }
)); ));
} else { } else {
@@ -1351,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.
@@ -1396,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.
@@ -1411,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.
@@ -1479,14 +1680,11 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
// or popups, since the canvas-space transform is unclamped. // or popups, since the canvas-space transform is unclamped.
{ {
const ImGuiStyle& style = ImGui::GetStyle(); const ImGuiStyle& style = ImGui::GetStyle();
// 3-dot icon: filled-dot count encodes how much is visible — // Icon area sized to match the 3-dot icon's fixed geometry (see
// All shows all 3 dots filled, Connected 2, Collapsed 1 — so the // DrawPinDisplayModeIcon); filled-dot count visually "funnels
// icon visually "funnels down" as the node collapses further. // down" as the node collapses further (All=3, Connected=2,
const float dotRadius = 3.0f; // Collapsed=1).
const float dotSpacing = 9.0f; const ImVec2 iconSize(9.0f * 2.0f + 3.0f * 2.0f, 3.0f * 2.0f);
const int dotCount = 3;
const int filledDots = 3 - static_cast<int>(pinDisplay.mode);
const ImVec2 iconSize(dotSpacing * (dotCount - 1) + dotRadius * 2.0f, dotRadius * 2.0f);
const ImVec2 btnMin = ImGui::GetCursorScreenPos(); const ImVec2 btnMin = ImGui::GetCursorScreenPos();
const ImVec2 btnMax(btnMin.x + iconSize.x + style.FramePadding.x * 2.0f, const ImVec2 btnMax(btnMin.x + iconSize.x + style.FramePadding.x * 2.0f,
btnMin.y + iconSize.y + style.FramePadding.y * 2.0f); btnMin.y + iconSize.y + style.FramePadding.y * 2.0f);
@@ -1501,18 +1699,8 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
: ImGuiCol_Button); : ImGuiCol_Button);
ImDrawList* dl = ImGui::GetWindowDrawList(); ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(btnMin, btnMax, frameColor, style.FrameRounding); dl->AddRectFilled(btnMin, btnMax, frameColor, style.FrameRounding);
DrawPinDisplayModeIcon(dl, btnMin, btnMax, pinDisplay.mode,
const ImU32 dotColor = ImGui::GetColorU32(ImGuiCol_Text); ImGui::GetColorU32(ImGuiCol_Text), ImGui::GetColorU32(ImGuiCol_TextDisabled));
const ImU32 dotHollowColor = ImGui::GetColorU32(ImGuiCol_TextDisabled);
ImVec2 dotCenter(btnMin.x + style.FramePadding.x + dotRadius,
btnMin.y + style.FramePadding.y + dotRadius);
for (int i = 0; i < dotCount; ++i) {
if (i < filledDots)
dl->AddCircleFilled(dotCenter, dotRadius, dotColor);
else
dl->AddCircle(dotCenter, dotRadius, dotHollowColor, 0, 1.2f);
dotCenter.x += dotSpacing;
}
static const char* kTooltips[3] = { static const char* kTooltips[3] = {
"Showing all pins - click to show connected pins only", "Showing all pins - click to show connected pins only",
@@ -1971,6 +2159,16 @@ void MaterialEditorPanel::CyclePinDisplayMode(const pxr::SdfPath& nodePath) {
PersistPinDisplayState(nodePath); 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 { uintptr_t MaterialEditorPanel::EffectivePinId(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const {
auto it = m_pinDisplayOverrides.find(nodePath.GetString()); auto it = m_pinDisplayOverrides.find(nodePath.GetString());
if (it != m_pinDisplayOverrides.end() && it->second.mode == PinDisplayMode::Collapsed) if (it != m_pinDisplayOverrides.end() && it->second.mode == PinDisplayMode::Collapsed)
@@ -2175,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();
} }
@@ -2189,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();
} }
+28
View File
@@ -108,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).
@@ -169,6 +177,8 @@ private:
bool IsPinVisible(const ShaderGraphNode& node, const std::string& pinName, bool isOutput) const; bool IsPinVisible(const ShaderGraphNode& node, const std::string& pinName, bool isOutput) const;
/// Cycles a node's display mode: All -> Connected -> Collapsed -> All. /// Cycles a node's display mode: All -> Connected -> Collapsed -> All.
void CyclePinDisplayMode(const pxr::SdfPath& nodePath); 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 /// Authors the node's current PinDisplayOverride into USD customData via
/// an undoable command, mirroring PersistNodePosition. No-op unless /// an undoable command, mirroring PersistNodePosition. No-op unless
/// m_keepGraphNodeViewSettingsInUsd is set. /// m_keepGraphNodeViewSettingsInUsd is set.
@@ -200,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
@@ -249,6 +268,15 @@ private:
bool m_openPinPickMenu = false; bool m_openPinPickMenu = false;
PendingPinPick m_pendingPinPick; 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);
+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();
} }
+11 -3
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:
@@ -24,9 +31,10 @@ private:
void RenderSublayerList(const std::vector<LayerInfo>& sublayers); void RenderSublayerList(const std::vector<LayerInfo>& sublayers);
void RenderSublayerContextMenu(int i, const std::vector<LayerInfo>& sublayers); void RenderSublayerContextMenu(int i, const std::vector<LayerInfo>& sublayers);
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;