Compare commits

...

4 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
33 changed files with 2223 additions and 95 deletions
+1
View File
@@ -1,5 +1,6 @@
# Build directories
build/
build_*/
install/
out/
.kilocode/
+3 -3
View File
@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Build
**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
# 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
```
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
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" `
-DWITH_CYCLES=OFF
```
+70 -40
View File
@@ -63,9 +63,13 @@ set(ARNOLD_LOCATION "" CACHE PATH "Arnold SDK root (e.g. C:/Autodesk/mtoa/5.5.0/
if(HDARNOLD_ROOT AND EXISTS "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
set(OPENUSD_HAS_ARNOLD TRUE)
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(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")
set(ARNOLD_AI_DLL "${ARNOLD_LOCATION}/bin/ai.dll")
# 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,
# 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")
# 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)
include(ExternalProject)
@@ -228,13 +238,20 @@ if(WIN32)
_CRT_SECURE_NO_WARNINGS
)
# OpenColorIO DLL — copy from USD bin/ (which already ships OpenColorIO_2_1.dll)
# OpenColorIO DLL — copy from USD bin/. Filename is version-suffixed
# (e.g. OpenColorIO_2_2.dll) and changes whenever the USD build's bundled
# OCIO version changes, so glob for it instead of hardcoding the version.
file(GLOB OPENCOLORIO_RUNTIME_DLL "${OpenUSD_ROOT_DIR}/bin/OpenColorIO_*.dll")
if(OPENCOLORIO_RUNTIME_DLL)
add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${OpenUSD_ROOT_DIR}/bin/OpenColorIO_2_1.dll"
${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
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"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${NDRARNOLD_DLL}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/ndrArnold.dll"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/nodeRegistryArnold.dll"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${HDARNOLD_PLUGIN_DIR}"
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${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..."
)
if(ARNOLD_RUNTIME_DLLS)
@@ -367,8 +390,10 @@ if(WIN32)
" endif()\n"
# Exclude debug-variant DLLs (not needed in release, just wasted space).
# Pattern: OpenColorIO_d_2_5.dll → contains "_d_"
# openjph.0.25d.dll → ends with "d.dll"
" if(_lname MATCHES \"_d_\" OR _lname MATCHES \"d[.]dll$\")\n"
# openjph.0.25d.dll → version digit directly followed by "d.dll"
# 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"
" endif()\n"
" list(APPEND _dlls \"\${_dll}\")\n"
@@ -391,6 +416,10 @@ if(WIN32)
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CYCLES_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..."
)
endif()
@@ -468,6 +497,14 @@ if(OPENUSD_HAS_ARNOLD)
DESTINATION bin/usd
OPTIONAL
)
install(FILES "${USDIMAGINGARNOLD_DLL}"
DESTINATION bin/usd
OPTIONAL
)
install(DIRECTORY "${USDIMAGINGARNOLD_PLUGIN_DIR}"
DESTINATION bin/usd
OPTIONAL
)
if(ARNOLD_RUNTIME_DLLS)
install(FILES ${ARNOLD_RUNTIME_DLLS}
DESTINATION bin
@@ -511,32 +548,20 @@ if(OPENUSD_HAS_CYCLES)
DESTINATION bin
OPTIONAL
)
# OpenColorIO_2_5.dll (used by Cycles' bundled OpenImageIO.dll) — same story
install(FILES "${CYCLES_OCIO25_DLL}"
DESTINATION bin
OPTIONAL
)
endif()
# Install Python runtime DLLs required by OpenUSD and Cycles.
# python312.dll : linked by Cycles (hdCycles.dll → python312.dll)
# python311.dll : linked by usd_python.dll (the OpenUSD Python bindings were
# 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()
# python312.dll: linked by both Cycles (hdCycles.dll) and USD's Python bindings
# (usd_python.dll) — the OpenUSD build and Cycles now share the same Python 3.12.
set(_python_runtime_dlls
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll"
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll"
)
if(PYTHON311_DLL)
list(APPEND _python_runtime_dlls "${PYTHON311_DLL}")
endif()
install(FILES ${_python_runtime_dlls}
DESTINATION bin
@@ -544,17 +569,6 @@ install(FILES ${_python_runtime_dlls}
# 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
# ---------------------------------------------------------------------------
@@ -662,7 +676,15 @@ add_custom_command(TARGET UsdLayerManager POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/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).
@@ -678,7 +700,7 @@ endif()
# Install OpenColorIO DLL (from the OpenUSD distribution's bin/)
install(FILES
"${OpenUSD_ROOT_DIR}/bin/OpenColorIO_2_1.dll"
${OPENCOLORIO_RUNTIME_DLL}
DESTINATION bin
OPTIONAL
)
@@ -689,6 +711,14 @@ install(DIRECTORY ${CMAKE_SOURCE_DIR}/resources
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/
# ImGuiContext tries "resources/font/Inter.ttf" then "resources/font/Inter.ttc".
install(FILES ${CMAKE_SOURCE_DIR}/third_party/Inter/Inter.ttc
+3 -4
View File
@@ -9,15 +9,14 @@
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"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",
"CMAKE_CXX_STANDARD": "17",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"BUILD_TESTS": "OFF",
"WITH_CYCLES": "ON",
"HDARNOLD_ROOT": "E:/library/hdArnold",
"ARNOLD_LOCATION": "C:/Autodesk/mtoa/5.5.0/2026",
"HDEMBREE_USD_ROOT": "E:/USD_v25.05_embree"
"HDARNOLD_ROOT": "E:/library/hdArnold-v25.11",
"ARNOLD_LOCATION": "C:/Autodesk/mtoa/5.5.0/2026"
}
},
{
+5 -5
View File
@@ -1,6 +1,6 @@
# USD Layer Manager
A C++17 / Windows desktop application providing a **Maya-style** USD scene and look-dev 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, 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.
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)
@@ -26,7 +26,7 @@ A C++17 / Windows desktop application providing a **Maya-style** USD scene and l
| 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 |
| [GLAD](https://glad.dav1d.de/) | — | OpenGL 3.3 core loader |
| Python | 3.12 | Required by OpenUSD runtime (`python312.dll`) |
@@ -35,8 +35,8 @@ A C++17 / Windows desktop application providing a **Maya-style** USD scene and l
| 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` |
| **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` |
| **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) (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 |
All delegate paths are pre-configured in `CMakePresets.json`.
@@ -166,7 +166,7 @@ Use Kilo's `openspec-*` skills to streamline this workflow.
Before writing any OpenUSD API call, verify the API exists in the actual SDK:
```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 manually verifying a change:
-1
View File
@@ -72,7 +72,6 @@ set(OpenUSD_REQUIRED_LIBS
hio
ar
kind
ndr
sdr
python
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」
——以追加的方式記錄後續進展或設計變動,而非直接覆寫、抹除本次
決策當下的紀錄。
+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

+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_backgroundColor(0.15f, 0.15f, 0.15f)
, m_shadingMode(ShadingMode::SmoothShaded)
, m_cullStyle(CullStyle::BackUnlessDoubleSided)
, m_cullStyle(CullStyle::Nothing) // matches usdview; see ViewportTileSettings::cullStyle
, m_colorCorrectionMode(ColorCorrectionMode::sRGB)
, m_ambientLightOnly(true)
, m_domeLightEnabled(false)
@@ -1,5 +1,6 @@
#include "ConnectShaderAttrsCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/usdShade/shader.h>
#include <pxr/usd/usdShade/connectableAPI.h>
#include <pxr/base/tf/token.h>
@@ -12,7 +13,8 @@ ConnectShaderAttrsCommand::ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfValueTypeName& destType,
const pxr::SdfPath& sourceNode,
const std::string& sourceOutput,
const pxr::SdfValueTypeName& sourceType)
const pxr::SdfValueTypeName& sourceType,
pxr::SdfLayerHandle editLayer)
: m_stage(stage)
, m_destNode(destNode)
, m_destInput(destInput)
@@ -20,6 +22,7 @@ ConnectShaderAttrsCommand::ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
, m_sourceNode(sourceNode)
, m_sourceOutput(sourceOutput)
, m_sourceType(sourceType)
, m_editLayer(editLayer)
, m_description("Connect " + sourceNode.GetName() + "." + sourceOutput +
" -> " + destNode.GetName() + "." + destInput)
{
@@ -50,9 +53,16 @@ void ConnectShaderAttrsCommand::Execute() {
return;
}
if (m_editLayer) {
pxr::UsdEditContext ec(m_stage, m_editLayer);
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) {
LOG_ERROR(std::string("ConnectShaderAttrsCommand::Execute error: ") + e.what());
}
@@ -66,6 +76,7 @@ void ConnectShaderAttrsCommand::Undo() {
pxr::UsdShadeInput destInput = destShader.GetInput(pxr::TfToken(m_destInput));
if (!destInput) return;
auto doUndo = [&]() {
if (m_hadPriorConnection) {
pxr::UsdShadeShader priorSourceShader(m_stage->GetPrimAtPath(m_priorSourceNode));
pxr::UsdShadeOutput priorSourceOutput = priorSourceShader.GetOutput(pxr::TfToken(m_priorSourceOutput));
@@ -74,6 +85,14 @@ void ConnectShaderAttrsCommand::Undo() {
} else {
pxr::UsdShadeConnectableAPI::DisconnectSource(destInput);
}
};
if (m_editLayer) {
pxr::UsdEditContext ec(m_stage, m_editLayer);
doUndo();
} else {
doUndo();
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("ConnectShaderAttrsCommand::Undo error: ") + e.what());
}
@@ -4,6 +4,7 @@
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/valueTypeName.h>
#include <pxr/usd/sdf/layer.h>
#include <string>
namespace UsdLayerManager {
@@ -14,13 +15,19 @@ namespace UsdLayerManager {
/// Undo can restore it exactly rather than merely disconnecting.
class ConnectShaderAttrsCommand : public ICommand {
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,
const pxr::SdfPath& destNode,
const std::string& destInput,
const pxr::SdfValueTypeName& destType,
const pxr::SdfPath& sourceNode,
const std::string& sourceOutput,
const pxr::SdfValueTypeName& sourceType);
const pxr::SdfValueTypeName& sourceType,
pxr::SdfLayerHandle editLayer = pxr::SdfLayerHandle());
void Execute() override;
void Undo() override;
@@ -34,6 +41,7 @@ private:
pxr::SdfPath m_sourceNode;
std::string m_sourceOutput;
pxr::SdfValueTypeName m_sourceType;
pxr::SdfLayerHandle m_editLayer;
std::string m_description;
bool m_hadPriorConnection = false;
@@ -1,5 +1,6 @@
#include "DisconnectShaderAttrCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/usdShade/shader.h>
#include <pxr/usd/usdShade/connectableAPI.h>
#include <pxr/base/tf/token.h>
@@ -8,10 +9,12 @@ namespace UsdLayerManager {
DisconnectShaderAttrCommand::DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& destNode,
const std::string& destInput)
const std::string& destInput,
pxr::SdfLayerHandle editLayer)
: m_stage(stage)
, m_destNode(destNode)
, m_destInput(destInput)
, m_editLayer(editLayer)
, m_description("Disconnect " + destNode.GetName() + "." + destInput)
{
if (!stage) return;
@@ -38,7 +41,13 @@ void DisconnectShaderAttrCommand::Execute() {
if (!destShader) return;
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput));
if (!input) return;
if (m_editLayer) {
pxr::UsdEditContext ec(m_stage, m_editLayer);
pxr::UsdShadeConnectableAPI::DisconnectSource(input);
} else {
pxr::UsdShadeConnectableAPI::DisconnectSource(input);
}
} catch (const std::exception& e) {
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::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);
} else {
pxr::UsdShadeConnectableAPI::ConnectToSource(input, output);
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("DisconnectShaderAttrCommand::Undo error: ") + e.what());
}
@@ -3,6 +3,7 @@
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/layer.h>
#include <string>
namespace UsdLayerManager {
@@ -11,9 +12,13 @@ namespace UsdLayerManager {
/// source at construction time so Undo can restore it.
class DisconnectShaderAttrCommand : public ICommand {
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,
const pxr::SdfPath& destNode,
const std::string& destInput);
const std::string& destInput,
pxr::SdfLayerHandle editLayer = pxr::SdfLayerHandle());
void Execute() override;
void Undo() override;
@@ -23,6 +28,7 @@ private:
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_destNode;
std::string m_destInput;
pxr::SdfLayerHandle m_editLayer;
std::string m_description;
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";
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;
WIN32_FIND_DATAA findData;
std::string searchPattern = pluginPath + "\\*";
+128
View File
@@ -41,6 +41,7 @@ Application::Application()
: m_showDemoWindow(false)
, m_showStageInfo(true)
, m_showStageEditor(true)
, m_showRenderLayerPanel(true)
, m_showSceneHierarchy(true)
, m_showViewport(true)
, m_showPropertyPanel(true)
@@ -67,15 +68,23 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
// Create managers
m_stageManager = std::make_unique<UsdStageManager>();
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->SetCommandHistory(&m_commandHistory);
m_stageEditorPanel = std::make_unique<StageEditorPanel>();
m_stageEditorPanel->SetLayerManager(m_layerManager.get());
m_stageEditorPanel->SetCommandHistory(&m_commandHistory);
m_stageEditorPanel->SetRenderLayerManager(m_renderLayerManager.get());
m_sceneHierarchyPanel = std::make_unique<SceneHierarchyPanel>();
m_sceneHierarchyPanel->SetPropertyManager(m_propertyManager.get());
m_sceneHierarchyPanel->SetCommandHistory(&m_commandHistory);
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->SetCommandHistory(&m_commandHistory);
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_stageEditorPanel->SetIconManager(m_iconManager.get());
m_materialEditorPanel->SetIconManager(m_iconManager.get());
m_renderLayerPanel->SetIconManager(m_iconManager.get());
m_sceneHierarchyPanel->SetOnPrimSelected(
[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.
if (const char* appData = getenv("APPDATA")) {
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_viewportPanel->LoadSettings(m_viewportSettingsPath);
LoadPreferences();
ValidateOcioPreferences();
// Apply pref delegate only to tiles that have no saved delegate.
m_viewportPanel->ApplyDefaultDelegate(m_prefs.renderDelegate);
// Color correction is always global — apply to all tiles.
@@ -223,10 +246,12 @@ void Application::Shutdown() {
m_sceneHierarchyPanel.reset();
m_propertyPanel.reset();
m_stageEditorPanel.reset();
m_renderLayerPanel.reset();
// Owns the shader-ball preview's Hydra engine + GL draw target — must be
// destroyed while the GL context still exists, like m_viewportPanel.
m_materialEditorPanel.reset();
m_propertyManager.reset();
m_renderLayerManager.reset();
m_layerManager.reset();
if (m_iconManager) {
@@ -251,8 +276,12 @@ void Application::RefreshManagers() {
if (m_stageManager->HasStage()) {
auto stage = m_stageManager->GetStage();
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_sceneHierarchyPanel->SetStage(stage);
m_renderLayerPanel->SetStage(stage);
m_viewportPanel->SetStage(stage);
m_viewportPanel->FrameScene();
m_propertyPanel->SetStage(stage);
@@ -260,10 +289,16 @@ void Application::RefreshManagers() {
m_curveEditorPanel->SetStage(stage);
m_materialManager->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 {
m_layerManager->SetStage(nullptr);
m_renderLayerManager->SetStage(nullptr);
m_propertyManager->SetStage(nullptr);
m_sceneHierarchyPanel->SetStage(nullptr);
m_renderLayerPanel->SetStage(nullptr);
m_viewportPanel->SetStage(nullptr);
m_propertyPanel->SetStage(nullptr);
m_timelinePanel->SetStage(nullptr);
@@ -315,6 +350,12 @@ void Application::RenderUI() {
ImGui::End();
}
if (m_showRenderLayerPanel) {
ImGui::Begin("Render Layers", &m_showRenderLayerPanel, ImGuiWindowFlags_NoCollapse);
m_renderLayerPanel->Render();
ImGui::End();
}
if (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()
{
// Pull the latest splitter positions; called from Shutdown before the
@@ -765,6 +876,7 @@ void Application::RenderMenuBar() {
if (ImGui::BeginMenu("View")) {
ImGui::MenuItem("Stage Editor", nullptr, &m_showStageEditor);
ImGui::MenuItem("Render Layers", nullptr, &m_showRenderLayerPanel);
ImGui::MenuItem("Scene Hierarchy", nullptr, &m_showSceneHierarchy);
ImGui::MenuItem("Viewport", nullptr, &m_showViewport);
ImGui::MenuItem("Property Panel", nullptr, &m_showPropertyPanel);
@@ -860,7 +972,9 @@ void Application::SaveUsdFile() {
if (!m_stageManager->SaveStage()) {
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
return;
}
SaveDirtyRenderLayers();
}
void Application::SaveUsdFileAs() {
@@ -879,6 +993,11 @@ void Application::SaveUsdFileAs() {
if (!m_stageManager->SaveStageAs(filePath)) {
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
} 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.
// RefreshManagers syncs m_layerManager (and others) to that new 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() {
m_stageManager->CloseStage();
// 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 "StageEditorPanel.h"
#include "SceneHierarchyPanel.h"
#include "RenderLayerPanel.h"
#include "ViewportPanel.h"
#include "PropertyPanel.h"
#include "TimelinePanel.h"
@@ -11,6 +12,7 @@
#include "MaterialEditorPanel.h"
#include "../core/UsdStageManager.h"
#include "../core/LayerManager.h"
#include "../core/RenderLayerManager.h"
#include "../core/PropertyManager.h"
#include "../core/MaterialManager.h"
#include "../core/CommandHistory.h"
@@ -65,12 +67,22 @@ private:
void AddReferenceToStage();
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
void CapturePlayblastFrame();
// Preferences
void LoadPreferences();
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 RenderPreferencesDialog();
@@ -78,11 +90,13 @@ private:
std::unique_ptr<IconManager> m_iconManager;
std::unique_ptr<UsdStageManager> m_stageManager;
std::unique_ptr<LayerManager> m_layerManager;
std::unique_ptr<RenderLayerManager> m_renderLayerManager;
std::unique_ptr<PropertyManager> m_propertyManager;
std::unique_ptr<MaterialManager> m_materialManager;
CommandHistory m_commandHistory;
std::unique_ptr<StageEditorPanel> m_stageEditorPanel;
std::unique_ptr<SceneHierarchyPanel> m_sceneHierarchyPanel;
std::unique_ptr<RenderLayerPanel> m_renderLayerPanel;
std::unique_ptr<ViewportPanel> m_viewportPanel;
std::unique_ptr<PropertyPanel> m_propertyPanel;
std::unique_ptr<TimelinePanel> m_timelinePanel;
@@ -91,6 +105,7 @@ private:
bool m_showDemoWindow;
bool m_showStageInfo;
bool m_showStageEditor;
bool m_showRenderLayerPanel;
bool m_showSceneHierarchy;
bool m_showViewport;
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::Pen: return "pen.svg";
case Icon::Settings: return "gear.svg";
case Icon::ChevronUp: return "chevron-up.svg";
case Icon::ChevronDown: return "chevron-down.svg";
default: return nullptr;
}
}
@@ -77,6 +79,7 @@ static constexpr Icon kAllIcons[] = {
Icon::SkipBack, Icon::StepBack, Icon::PlayBack, Icon::Play, Icon::Pause,
Icon::StepForward, Icon::SkipEnd, Icon::Loop, Icon::Bounce,
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
Pen, // edit / edit target
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
+28 -5
View File
@@ -9,6 +9,7 @@
#include "../utils/Logger.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/material.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
@@ -1428,25 +1429,42 @@ void MaterialEditorPanel::BindMaterialToTarget(const pxr::SdfPath& materialPath,
}
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::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);
} else {
pxr::UsdShadeMaterialBindingAPI::Apply(target).Bind(material);
}
};
if (m_commandHistory) {
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
"Bind Material",
doBind,
[stage, targetPath, hadPriorRel, priorTargets]() {
[stage, targetPath, hadPriorRel, priorTargets, editLayer]() {
pxr::UsdPrim target = stage->GetPrimAtPath(targetPath);
if (!target) return;
auto doUnbind = [&]() {
pxr::UsdShadeMaterialBindingAPI bindAPI(target);
if (hadPriorRel && !priorTargets.empty())
bindAPI.GetDirectBindingRel().SetTargets(priorTargets);
else
bindAPI.UnbindDirectBinding();
};
if (editLayer) {
pxr::UsdEditContext ec(stage, editLayer);
doUnbind();
} else {
doUnbind();
}
}
));
} else {
@@ -2355,9 +2373,13 @@ pxr::SdfPath MaterialEditorPanel::CreateShaderNode(const std::string& shaderId,
void MaterialEditorPanel::CreateConnection(const PinInfo& destInput, const PinInfo& sourceOutput) {
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_stage, destInput.nodePath, destInput.name, destInput.typeName,
sourceOutput.nodePath, sourceOutput.name, sourceOutput.typeName));
sourceOutput.nodePath, sourceOutput.name, sourceOutput.typeName, editLayer));
SyncFromUsd();
}
@@ -2369,7 +2391,8 @@ void MaterialEditorPanel::DeleteNode(const pxr::SdfPath& nodePath) {
void MaterialEditorPanel::DisconnectAttr(const pxr::SdfPath& destNode, const std::string& destInput) {
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();
}
+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/ReparentPrimCommand.h"
#include "../core/commands/GroupPrimsCommand.h"
#include "../core/commands/LayerCommands.h"
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/primRange.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);
if (ImGui::BeginCombo("##layerswitch", currentLabel.c_str(),
ImGuiComboFlags_HeightRegular)) {
@@ -326,9 +329,16 @@ void SceneHierarchyPanel::Render() {
}
ImGui::EndCombo();
}
bool comboHovered = ImGui::IsItemHovered();
ImGui::EndDisabled();
if (renderLayerActive && comboHovered)
ImGui::SetTooltip("Edit target is locked to the active render layer");
ImGui::Spacing();
}
RenderSublayerSection();
ImGui::Spacing();
auto paths = m_propertyManager->GetPrimPaths();
if (paths.empty()) {
ImGui::TextDisabled("No prims in stage");
@@ -582,6 +592,272 @@ void SceneHierarchyPanel::Render() {
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) {
if (!prim.IsValid()) return;
+18
View File
@@ -3,6 +3,7 @@
#include "../core/PropertyManager.h"
#include "../core/CommandHistory.h"
#include "../core/LayerManager.h"
#include "../core/RenderLayerManager.h"
#include "IconManager.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
@@ -26,6 +27,11 @@ public:
void SetPropertyManager(PropertyManager* manager);
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
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 SetIconManager(IconManager* iconManager) { m_iconManager = iconManager; }
void Render();
@@ -33,6 +39,12 @@ public:
std::string GetSelectedPrimPath() const { return m_primarySelectedPath; }
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).
/// Also clears any rect multi-selection.
void SetSelectedPathFromClick(const std::string& path);
@@ -79,10 +91,13 @@ private:
void RenderRemovePrimModal();
void RenderArcModal();
void ProcessPendingReplaceRef();
void RenderSublayerSection();
void RenderSublayerContextMenu(int i, const std::vector<LayerInfo>& sublayers);
PropertyManager* m_propertyManager;
CommandHistory* m_commandHistory = nullptr;
LayerManager* m_layerManager = nullptr;
RenderLayerManager* m_renderLayerManager = nullptr;
IconManager* m_iconManager = nullptr;
UsdStageRefPtr m_stage;
@@ -94,6 +109,9 @@ private:
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.
/// Contains only the stage's own layers (root + sublayers + session),
/// 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);
{
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
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(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();
@@ -242,6 +247,9 @@ void StageEditorPanel::RenderSublayerList(const std::vector<LayerInfo>& sublayer
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
@@ -250,6 +258,12 @@ void StageEditorPanel::RenderSublayerList(const std::vector<LayerInfo>& sublayer
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).
@@ -257,23 +271,32 @@ void StageEditorPanel::RenderSublayerList(const std::vector<LayerInfo>& sublayer
{
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(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);
{
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");
}
ImGui::PopID();
@@ -289,14 +312,21 @@ void StageEditorPanel::RenderSublayerContextMenu(int i,
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")) {
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"))
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();
@@ -337,13 +367,15 @@ void StageEditorPanel::RenderSublayerContextMenu(int i,
ImGui::Separator();
if (ImGui::MenuItem("Remove")) {
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();
}
+8
View File
@@ -2,6 +2,7 @@
#include "../core/LayerManager.h"
#include "../core/CommandHistory.h"
#include "../core/RenderLayerManager.h"
#include "IconManager.h"
#include <imgui.h>
#include <memory>
@@ -17,6 +18,12 @@ public:
void SetLayerManager(LayerManager* manager) { m_layerManager = manager; }
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
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();
private:
@@ -27,6 +34,7 @@ private:
LayerManager* m_layerManager = nullptr;
CommandHistory* m_commandHistory = nullptr;
IconManager* m_iconManager = nullptr;
RenderLayerManager* m_renderLayerManager = nullptr;
int m_selectedIdx = -1;
};
+5 -1
View File
@@ -32,7 +32,11 @@ struct ViewportTileSettings {
bool ambientLightOnly = true;
bool domeLightEnabled = false;
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 showProxy = true;
bool showRender = false;