Compare commits
13 Commits
d9826b7bbb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 05201465be | |||
| a907ec0182 | |||
| b5c1e50438 | |||
| e2a4961ebe | |||
| 9d61819636 | |||
| 27c619c518 | |||
| 196a2bb0dd | |||
| 6cb1b45496 | |||
| a877e8973c | |||
| a7314ff108 | |||
| bff42082a5 | |||
| c7edd145df | |||
| 22406fbe63 |
@@ -1,5 +1,6 @@
|
|||||||
# Build directories
|
# Build directories
|
||||||
build/
|
build/
|
||||||
|
build_*/
|
||||||
install/
|
install/
|
||||||
out/
|
out/
|
||||||
.kilocode/
|
.kilocode/
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
## Build
|
## Build
|
||||||
|
|
||||||
**Prerequisites:** MSVC 2022, CMake ≥ 3.20, Python 3.12 at `%LOCALAPPDATA%\Programs\Python\Python312`.
|
**Prerequisites:** MSVC 2022, CMake ≥ 3.20, Python 3.12 at `%LOCALAPPDATA%\Programs\Python\Python312`.
|
||||||
OpenUSD 25.05 is expected at `third_party/OpenUSD-v25.05` (set via `CMAKE_PREFIX_PATH` in the preset).
|
OpenUSD 25.11 is expected at `third_party/OpenUSD-v25.11` (set via `CMAKE_PREFIX_PATH` in the preset).
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Configure (one-time; downloads ACES 1.2 OCIO config ~124 MB on first run)
|
# Configure (one-time; downloads ACES 1.2 OCIO config ~124 MB on first run)
|
||||||
@@ -20,11 +20,11 @@ cmake --build build --config Release
|
|||||||
cmake --build build --config Release --target install
|
cmake --build build --config Release --target install
|
||||||
```
|
```
|
||||||
|
|
||||||
The `default` preset (in `CMakePresets.json`) enables `WITH_CYCLES=ON`, `HDARNOLD_ROOT`, and `HDEMBREE_USD_ROOT` for the maintainer's machine. For a minimal build without optional render delegates, configure manually:
|
The `default` preset (in `CMakePresets.json`) enables `WITH_CYCLES=ON` and `HDARNOLD_ROOT` for the maintainer's machine. hdEmbree is bundled directly in the `third_party/OpenUSD-v25.11` build (built with `--embree`), so no separate `HDEMBREE_USD_ROOT` is needed. For a minimal build without optional render delegates, configure manually:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cmake -B build -G "Visual Studio 17 2022" `
|
cmake -B build -G "Visual Studio 17 2022" `
|
||||||
-DCMAKE_PREFIX_PATH="third_party/OpenUSD-v25.05" `
|
-DCMAKE_PREFIX_PATH="third_party/OpenUSD-v25.11" `
|
||||||
-DIMGUI_DIR="third_party/imgui-1.92.7" `
|
-DIMGUI_DIR="third_party/imgui-1.92.7" `
|
||||||
-DWITH_CYCLES=OFF
|
-DWITH_CYCLES=OFF
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -63,9 +63,13 @@ set(ARNOLD_LOCATION "" CACHE PATH "Arnold SDK root (e.g. C:/Autodesk/mtoa/5.5.0/
|
|||||||
if(HDARNOLD_ROOT AND EXISTS "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
|
if(HDARNOLD_ROOT AND EXISTS "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
|
||||||
set(OPENUSD_HAS_ARNOLD TRUE)
|
set(OPENUSD_HAS_ARNOLD TRUE)
|
||||||
set(HDARNOLD_DLL "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
|
set(HDARNOLD_DLL "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
|
||||||
set(NDRARNOLD_DLL "${HDARNOLD_ROOT}/plugin/ndrArnold.dll")
|
|
||||||
set(HDARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/hdArnold")
|
set(HDARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/hdArnold")
|
||||||
set(NDRARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/ndrArnold")
|
# arnold-usd renamed the ndr plugin to node_registry (USD merged ndr into
|
||||||
|
# sdr as of PXR_VERSION 2505) and added a usdImaging adapter plugin.
|
||||||
|
set(NDRARNOLD_DLL "${HDARNOLD_ROOT}/plugin/nodeRegistryArnold.dll")
|
||||||
|
set(NDRARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/nodeRegistryArnold")
|
||||||
|
set(USDIMAGINGARNOLD_DLL "${HDARNOLD_ROOT}/plugin/usdImagingArnold.dll")
|
||||||
|
set(USDIMAGINGARNOLD_PLUGIN_DIR "${HDARNOLD_ROOT}/plugin/usdImagingArnold")
|
||||||
if(ARNOLD_LOCATION AND EXISTS "${ARNOLD_LOCATION}/bin/ai.dll")
|
if(ARNOLD_LOCATION AND EXISTS "${ARNOLD_LOCATION}/bin/ai.dll")
|
||||||
set(ARNOLD_AI_DLL "${ARNOLD_LOCATION}/bin/ai.dll")
|
set(ARNOLD_AI_DLL "${ARNOLD_LOCATION}/bin/ai.dll")
|
||||||
# ai.dll also requires companion runtime DLLs (OIDN denoiser, Adsk licensing, cer)
|
# ai.dll also requires companion runtime DLLs (OIDN denoiser, Adsk licensing, cer)
|
||||||
@@ -98,6 +102,12 @@ set(HDCYCLES_PLUGIN_DIR "${CYCLES_INSTALL_DIR}/hydra/hdCycles")
|
|||||||
# tbb12.dll is required by openvdb/embree4 but lives in the precompiled lib tree,
|
# tbb12.dll is required by openvdb/embree4 but lives in the precompiled lib tree,
|
||||||
# not in the install output — deploy it explicitly.
|
# not in the install output — deploy it explicitly.
|
||||||
set(CYCLES_TBB12_DLL "${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/tbb/bin/tbb12.dll")
|
set(CYCLES_TBB12_DLL "${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/tbb/bin/tbb12.dll")
|
||||||
|
# OpenColorIO_2_5.dll: Cycles' bundled prebuilt OpenImageIO.dll was compiled
|
||||||
|
# against Cycles' own OCIO 2.5 (forced via -DOpenColorIO_DIR below, to avoid
|
||||||
|
# USD's OCIO headers shadowing Cycles' during the ExternalProject build) —
|
||||||
|
# distinct from and required alongside USD's own OpenColorIO_2_2.dll. It also
|
||||||
|
# lives only in the precompiled lib tree, not in cycles_install's output.
|
||||||
|
set(CYCLES_OCIO25_DLL "${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/opencolorio/bin/OpenColorIO_2_5.dll")
|
||||||
|
|
||||||
if(WITH_CYCLES)
|
if(WITH_CYCLES)
|
||||||
include(ExternalProject)
|
include(ExternalProject)
|
||||||
@@ -228,13 +238,20 @@ if(WIN32)
|
|||||||
_CRT_SECURE_NO_WARNINGS
|
_CRT_SECURE_NO_WARNINGS
|
||||||
)
|
)
|
||||||
|
|
||||||
# OpenColorIO DLL — copy from USD bin/ (which already ships OpenColorIO_2_1.dll)
|
# OpenColorIO DLL — copy from USD bin/. Filename is version-suffixed
|
||||||
|
# (e.g. OpenColorIO_2_2.dll) and changes whenever the USD build's bundled
|
||||||
|
# OCIO version changes, so glob for it instead of hardcoding the version.
|
||||||
|
file(GLOB OPENCOLORIO_RUNTIME_DLL "${OpenUSD_ROOT_DIR}/bin/OpenColorIO_*.dll")
|
||||||
|
if(OPENCOLORIO_RUNTIME_DLL)
|
||||||
add_custom_command(TARGET UsdLayerManager POST_BUILD
|
add_custom_command(TARGET UsdLayerManager POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
"${OpenUSD_ROOT_DIR}/bin/OpenColorIO_2_1.dll"
|
${OPENCOLORIO_RUNTIME_DLL}
|
||||||
"$<TARGET_FILE_DIR:UsdLayerManager>"
|
"$<TARGET_FILE_DIR:UsdLayerManager>"
|
||||||
COMMENT "Copying OpenColorIO runtime DLL..."
|
COMMENT "Copying OpenColorIO runtime DLL..."
|
||||||
)
|
)
|
||||||
|
else()
|
||||||
|
message(WARNING "OpenColorIO runtime DLL not found in ${OpenUSD_ROOT_DIR}/bin")
|
||||||
|
endif()
|
||||||
|
|
||||||
# FFmpeg DLLs — copy all .dll files from third_party/ffmpeg/bin
|
# FFmpeg DLLs — copy all .dll files from third_party/ffmpeg/bin
|
||||||
file(GLOB FFMPEG_RUNTIME_DLLS "${CMAKE_SOURCE_DIR}/third_party/ffmpeg/bin/*.dll")
|
file(GLOB FFMPEG_RUNTIME_DLLS "${CMAKE_SOURCE_DIR}/third_party/ffmpeg/bin/*.dll")
|
||||||
@@ -323,13 +340,19 @@ if(WIN32)
|
|||||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold.dll"
|
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold.dll"
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
"${NDRARNOLD_DLL}"
|
"${NDRARNOLD_DLL}"
|
||||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/ndrArnold.dll"
|
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/nodeRegistryArnold.dll"
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
"${HDARNOLD_PLUGIN_DIR}"
|
"${HDARNOLD_PLUGIN_DIR}"
|
||||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold"
|
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold"
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
"${NDRARNOLD_PLUGIN_DIR}"
|
"${NDRARNOLD_PLUGIN_DIR}"
|
||||||
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/ndrArnold"
|
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/nodeRegistryArnold"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${USDIMAGINGARNOLD_DLL}"
|
||||||
|
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/usdImagingArnold.dll"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
"${USDIMAGINGARNOLD_PLUGIN_DIR}"
|
||||||
|
"$<TARGET_FILE_DIR:UsdLayerManager>/usd/usdImagingArnold"
|
||||||
COMMENT "Copying hdArnold plugin..."
|
COMMENT "Copying hdArnold plugin..."
|
||||||
)
|
)
|
||||||
if(ARNOLD_RUNTIME_DLLS)
|
if(ARNOLD_RUNTIME_DLLS)
|
||||||
@@ -367,8 +390,10 @@ if(WIN32)
|
|||||||
" endif()\n"
|
" endif()\n"
|
||||||
# Exclude debug-variant DLLs (not needed in release, just wasted space).
|
# Exclude debug-variant DLLs (not needed in release, just wasted space).
|
||||||
# Pattern: OpenColorIO_d_2_5.dll → contains "_d_"
|
# Pattern: OpenColorIO_d_2_5.dll → contains "_d_"
|
||||||
# openjph.0.25d.dll → ends with "d.dll"
|
# openjph.0.25d.dll → version digit directly followed by "d.dll"
|
||||||
" if(_lname MATCHES \"_d_\" OR _lname MATCHES \"d[.]dll$\")\n"
|
# NOTE: must require a digit before the trailing "d" — a bare "d[.]dll$"
|
||||||
|
# also matches legitimate Release DLLs like IlmThread.dll (ends in "...ead.dll").
|
||||||
|
" if(_lname MATCHES \"_d_\" OR _lname MATCHES \"[0-9]d[.]dll$\")\n"
|
||||||
" continue()\n"
|
" continue()\n"
|
||||||
" endif()\n"
|
" endif()\n"
|
||||||
" list(APPEND _dlls \"\${_dll}\")\n"
|
" list(APPEND _dlls \"\${_dll}\")\n"
|
||||||
@@ -391,6 +416,10 @@ if(WIN32)
|
|||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
"${CYCLES_TBB12_DLL}"
|
"${CYCLES_TBB12_DLL}"
|
||||||
"$<TARGET_FILE_DIR:UsdLayerManager>/tbb12.dll"
|
"$<TARGET_FILE_DIR:UsdLayerManager>/tbb12.dll"
|
||||||
|
# OpenColorIO_2_5.dll is required by Cycles' bundled OpenImageIO.dll
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${CYCLES_OCIO25_DLL}"
|
||||||
|
"$<TARGET_FILE_DIR:UsdLayerManager>/OpenColorIO_2_5.dll"
|
||||||
COMMENT "Copying hdCycles plugin and runtime DLLs..."
|
COMMENT "Copying hdCycles plugin and runtime DLLs..."
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
@@ -468,6 +497,14 @@ if(OPENUSD_HAS_ARNOLD)
|
|||||||
DESTINATION bin/usd
|
DESTINATION bin/usd
|
||||||
OPTIONAL
|
OPTIONAL
|
||||||
)
|
)
|
||||||
|
install(FILES "${USDIMAGINGARNOLD_DLL}"
|
||||||
|
DESTINATION bin/usd
|
||||||
|
OPTIONAL
|
||||||
|
)
|
||||||
|
install(DIRECTORY "${USDIMAGINGARNOLD_PLUGIN_DIR}"
|
||||||
|
DESTINATION bin/usd
|
||||||
|
OPTIONAL
|
||||||
|
)
|
||||||
if(ARNOLD_RUNTIME_DLLS)
|
if(ARNOLD_RUNTIME_DLLS)
|
||||||
install(FILES ${ARNOLD_RUNTIME_DLLS}
|
install(FILES ${ARNOLD_RUNTIME_DLLS}
|
||||||
DESTINATION bin
|
DESTINATION bin
|
||||||
@@ -511,32 +548,20 @@ if(OPENUSD_HAS_CYCLES)
|
|||||||
DESTINATION bin
|
DESTINATION bin
|
||||||
OPTIONAL
|
OPTIONAL
|
||||||
)
|
)
|
||||||
|
# OpenColorIO_2_5.dll (used by Cycles' bundled OpenImageIO.dll) — same story
|
||||||
|
install(FILES "${CYCLES_OCIO25_DLL}"
|
||||||
|
DESTINATION bin
|
||||||
|
OPTIONAL
|
||||||
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Install Python runtime DLLs required by OpenUSD and Cycles.
|
# Install Python runtime DLLs required by OpenUSD and Cycles.
|
||||||
# python312.dll : linked by Cycles (hdCycles.dll → python312.dll)
|
# python312.dll: linked by both Cycles (hdCycles.dll) and USD's Python bindings
|
||||||
# python311.dll : linked by usd_python.dll (the OpenUSD Python bindings were
|
# (usd_python.dll) — the OpenUSD build and Cycles now share the same Python 3.12.
|
||||||
# built against Python 3.11, regardless of what we compile with)
|
|
||||||
# Detect python311.dll from common install paths.
|
|
||||||
foreach(_pyroot
|
|
||||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python311"
|
|
||||||
"C:/Python311"
|
|
||||||
"C:/Program Files/Python311"
|
|
||||||
)
|
|
||||||
if(EXISTS "${_pyroot}/python311.dll")
|
|
||||||
set(PYTHON311_DLL "${_pyroot}/python311.dll")
|
|
||||||
message(STATUS "Found python311.dll (required by usd_python.dll): ${PYTHON311_DLL}")
|
|
||||||
break()
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
set(_python_runtime_dlls
|
set(_python_runtime_dlls
|
||||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll"
|
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll"
|
||||||
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll"
|
"$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll"
|
||||||
)
|
)
|
||||||
if(PYTHON311_DLL)
|
|
||||||
list(APPEND _python_runtime_dlls "${PYTHON311_DLL}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
install(FILES ${_python_runtime_dlls}
|
install(FILES ${_python_runtime_dlls}
|
||||||
DESTINATION bin
|
DESTINATION bin
|
||||||
@@ -544,17 +569,6 @@ install(FILES ${_python_runtime_dlls}
|
|||||||
# system-wide Python; in that case the DLL is on PATH already.
|
# system-wide Python; in that case the DLL is on PATH already.
|
||||||
)
|
)
|
||||||
|
|
||||||
# Also copy python311.dll to the build output directory so debug runs work
|
|
||||||
# without requiring C:\Python311 to be on PATH.
|
|
||||||
if(PYTHON311_DLL)
|
|
||||||
add_custom_command(TARGET UsdLayerManager POST_BUILD
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
|
||||||
"${PYTHON311_DLL}"
|
|
||||||
"$<TARGET_FILE_DIR:UsdLayerManager>/python311.dll"
|
|
||||||
COMMENT "Copying python311.dll (for usd_python.dll)..."
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# OCIO: download ACES 1.2 config at configure time if not present
|
# OCIO: download ACES 1.2 config at configure time if not present
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -662,7 +676,15 @@ add_custom_command(TARGET UsdLayerManager POST_BUILD
|
|||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
"${CMAKE_SOURCE_DIR}/resources/vdb"
|
"${CMAKE_SOURCE_DIR}/resources/vdb"
|
||||||
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/vdb"
|
"$<TARGET_FILE_DIR:UsdLayerManager>/resources/vdb"
|
||||||
COMMENT "Copying fonts, SVG icons, HDRI presets and preview shapes to build output"
|
# MaterialX standard library (nodedefs + genosl headers such as mx_funcs.h).
|
||||||
|
# hdArnold reads PXR_MTLX_STDLIB_SEARCH_PATHS to locate these; without them
|
||||||
|
# Arnold's OSL compile of MaterialX shaders fails with
|
||||||
|
# "fatal error: 'mx_funcs.h' file not found". Application.cpp points the
|
||||||
|
# env var at this deployed copy.
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
"${OpenUSD_ROOT_DIR}/libraries"
|
||||||
|
"$<TARGET_FILE_DIR:UsdLayerManager>/libraries"
|
||||||
|
COMMENT "Copying fonts, SVG icons, HDRI presets, preview shapes and MaterialX stdlib to build output"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Copy ACES OCIO config to build output — only on first build (skipped if already present).
|
# Copy ACES OCIO config to build output — only on first build (skipped if already present).
|
||||||
@@ -678,7 +700,7 @@ endif()
|
|||||||
|
|
||||||
# Install OpenColorIO DLL (from the OpenUSD distribution's bin/)
|
# Install OpenColorIO DLL (from the OpenUSD distribution's bin/)
|
||||||
install(FILES
|
install(FILES
|
||||||
"${OpenUSD_ROOT_DIR}/bin/OpenColorIO_2_1.dll"
|
${OPENCOLORIO_RUNTIME_DLL}
|
||||||
DESTINATION bin
|
DESTINATION bin
|
||||||
OPTIONAL
|
OPTIONAL
|
||||||
)
|
)
|
||||||
@@ -689,6 +711,14 @@ install(DIRECTORY ${CMAKE_SOURCE_DIR}/resources
|
|||||||
DESTINATION bin
|
DESTINATION bin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# MaterialX standard library — required by hdArnold's OSL codegen (mx_funcs.h).
|
||||||
|
# See the matching POST_BUILD copy above and PXR_MTLX_STDLIB_SEARCH_PATHS in
|
||||||
|
# Application.cpp.
|
||||||
|
install(DIRECTORY "${OpenUSD_ROOT_DIR}/libraries"
|
||||||
|
DESTINATION bin
|
||||||
|
OPTIONAL
|
||||||
|
)
|
||||||
|
|
||||||
# Install Inter font next to the exe under resources/font/
|
# Install Inter font next to the exe under resources/font/
|
||||||
# ImGuiContext tries "resources/font/Inter.ttf" then "resources/font/Inter.ttc".
|
# ImGuiContext tries "resources/font/Inter.ttf" then "resources/font/Inter.ttc".
|
||||||
install(FILES ${CMAKE_SOURCE_DIR}/third_party/Inter/Inter.ttc
|
install(FILES ${CMAKE_SOURCE_DIR}/third_party/Inter/Inter.ttc
|
||||||
|
|||||||
@@ -9,15 +9,14 @@
|
|||||||
"binaryDir": "${sourceDir}/build",
|
"binaryDir": "${sourceDir}/build",
|
||||||
"cacheVariables": {
|
"cacheVariables": {
|
||||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/install",
|
"CMAKE_INSTALL_PREFIX": "${sourceDir}/install",
|
||||||
"CMAKE_PREFIX_PATH": "${sourceDir}/third_party/OpenUSD-v25.05",
|
"CMAKE_PREFIX_PATH": "${sourceDir}/third_party/OpenUSD-v25.11",
|
||||||
"IMGUI_DIR": "${sourceDir}/third_party/imgui-1.92.7",
|
"IMGUI_DIR": "${sourceDir}/third_party/imgui-1.92.7",
|
||||||
"CMAKE_CXX_STANDARD": "17",
|
"CMAKE_CXX_STANDARD": "17",
|
||||||
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
|
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
|
||||||
"BUILD_TESTS": "OFF",
|
"BUILD_TESTS": "OFF",
|
||||||
"WITH_CYCLES": "ON",
|
"WITH_CYCLES": "ON",
|
||||||
"HDARNOLD_ROOT": "E:/library/hdArnold",
|
"HDARNOLD_ROOT": "E:/library/hdArnold-v25.11",
|
||||||
"ARNOLD_LOCATION": "C:/Autodesk/mtoa/5.5.0/2026",
|
"ARNOLD_LOCATION": "C:/Autodesk/mtoa/5.5.0/2026"
|
||||||
"HDEMBREE_USD_ROOT": "E:/USD_v25.05_embree"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -36,7 +35,7 @@
|
|||||||
"description": "Optimized release build",
|
"description": "Optimized release build",
|
||||||
"inherits": "default",
|
"inherits": "default",
|
||||||
"cacheVariables": {
|
"cacheVariables": {
|
||||||
"CMAKE_BUILD_TYPE": "Release",
|
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
|
||||||
"BUILD_TESTS": "OFF"
|
"BUILD_TESTS": "OFF"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
# USD Layer Manager
|
# USD Layer Manager
|
||||||
|
|
||||||
A C++17 / Windows desktop application providing a **Maya Render Layers-style** USD scene editor using **OpenUSD v25.05**, **ImGui 1.92.7** (docking branch), and **OpenGL 3.3** (via GLAD). Users can open, edit, and save USD files, manage layered stage overrides, inspect and edit prim properties, and interact with 3D scenes through a Hydra viewport.
|
A C++17 / Windows desktop application providing a **Maya-style** USD scene and look-dev editor using **OpenUSD v25.11**, **ImGui 1.92.7** (docking branch), and **OpenGL 3.3** (via GLAD). Users can open, edit, and save USD files, manage layered stage overrides, inspect and edit prim properties, author UsdShade material graphs with a live shader-ball preview, animate with a Bezier curve editor, and interact with 3D scenes through a multi-viewport Hydra renderer with OCIO color management.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Layer Stack Management** — Create, reorder, mute/unmute sublayers with full undo support
|
- **Layer Stack Management** — `StageEditorPanel`: create, reorder, mute/unmute sublayers, set the edit target, add existing files as sublayers, with full undo support
|
||||||
- **Scene Hierarchy** — Browse prim tree with per-type SVG icons; create, delete, rename prims; add/replace references
|
- **Scene Hierarchy** — Browse prim tree with per-type SVG icons; create, delete, rename, reparent, and group prims; add/replace references
|
||||||
- **Property Editor** — Maya Channel Box-style attribute editing with type-aware drag inputs, color pickers, and undoable transforms
|
- **Property Editor** — Maya Channel Box-style attribute editing with type-aware drag inputs, color pickers, and undoable transforms
|
||||||
- **3D Viewport** — Hydra rendering via `UsdImagingGLEngine` into an offscreen FBO; switchable render delegates (Storm, Embree, Arnold, Cycles); grid overlay; selection bounding boxes; camera wireframes
|
- **Material Editor** — Hypershade-style node-graph editor for UsdShade/MaterialX networks (`MaterialEditorPanel`): material browser, drag-to-connect node canvas, per-node/per-pin display modes, Sdr-driven node creation menu and enum dropdowns, colorSpace authoring on texture inputs, and a live shader-ball preview (`MaterialPreviewRenderer`) with its own renderer/HDRI/preview-shape controls. In-canvas node thumbnails (`NodeThumbnailCache`) show texture previews and per-node shader-ball renders, amortized across frames and re-rendered until progressive delegates (Arnold/Cycles/Embree) fully converge
|
||||||
- **Multi-Viewport** — Single, horizontal split, vertical split, and quad layouts; per-tile camera and render settings; Space to maximize/restore
|
- **Timeline & Curve Editor** — Transport controls with loop/bounce and Auto-Key (`TimelinePanel`); a Maya-style F-curve editor (`CurveEditorPanel`) with Bezier round-trip to USD time samples; Playblast export to MP4 via `MovieEncoder` (libavcodec)
|
||||||
|
- **3D Viewport** — Hydra rendering via `UsdImagingGLEngine` into an offscreen FBO; switchable render delegates (Storm, Embree, Arnold, Cycles); OCIO-based color management (sRGB or full display/view/look pipeline) applied as a post-Hydra GL pass; grid overlay; selection bounding boxes; camera/light wireframes
|
||||||
|
- **Multi-Viewport** — Single, horizontal split, vertical split, and quad layouts; per-tile camera, render delegate, and AOV; Space to maximize/restore
|
||||||
- **Camera Control** — Free orbital camera (tumble/truck/dolly) or drive from USD camera prims; auto near/far clipping; frame selection; correct camera prim orientation for Y-up and Z-up stages
|
- **Camera Control** — Free orbital camera (tumble/truck/dolly) or drive from USD camera prims; auto near/far clipping; frame selection; correct camera prim orientation for Y-up and Z-up stages
|
||||||
- **Transform Gizmo** — Pure ImDrawList-based manipulator (translate/rotate/scale) in object or world space
|
- **Transform Gizmo** — Pure ImDrawList-based manipulator (translate/rotate/scale) in object or world space
|
||||||
- **Undo/Redo** — Full command history for prim creation/deletion, attribute edits, transform changes, and layer operations; Ctrl+Z / Ctrl+Y hotkeys; Edit menu integration
|
- **Undo/Redo** — Full command history (`ICommand` subclasses under `src/core/commands/`) covering prim creation/deletion/reparenting/grouping, attribute edits, transform changes, shader-node creation/connection, and layer operations; Ctrl+Z / Ctrl+Y hotkeys; Edit menu integration
|
||||||
- **Multi-select** — Rectangular selection in viewport with cross-panel syncing
|
- **Multi-select** — Rectangular selection in viewport with cross-panel syncing
|
||||||
|
|
||||||
|
| Material Editor (Arnold) | Curve Editor | Quad Viewport (4 delegates) | MaterialX import |
|
||||||
|
|---|---|---|---|
|
||||||
|
|  |  |  |  |
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
| Dependency | Version | Notes |
|
| Dependency | Version | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| [OpenUSD](https://github.com/PixarAnimationStudios/OpenUSD) | v25.05 | Prebuilt; found via `FindOpenUSD.cmake` |
|
| [OpenUSD](https://github.com/PixarAnimationStudios/OpenUSD) | v25.11 | Prebuilt; found via `FindOpenUSD.cmake` |
|
||||||
| [Dear ImGui](https://github.com/ocornut/imgui) | v1.92.7 | Docking branch; Win32 + OpenGL3 backends |
|
| [Dear ImGui](https://github.com/ocornut/imgui) | v1.92.7 | Docking branch; Win32 + OpenGL3 backends |
|
||||||
| [GLAD](https://glad.dav1d.de/) | — | OpenGL 3.3 core loader |
|
| [GLAD](https://glad.dav1d.de/) | — | OpenGL 3.3 core loader |
|
||||||
| Python | 3.12 | Required by OpenUSD runtime (`python312.dll`) |
|
| Python | 3.12 | Required by OpenUSD runtime (`python312.dll`) |
|
||||||
@@ -29,8 +35,8 @@ A C++17 / Windows desktop application providing a **Maya Render Layers-style** U
|
|||||||
|
|
||||||
| Delegate | Renderer Version | Variable | Notes |
|
| Delegate | Renderer Version | Variable | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **hdEmbree** | Embree 4.4.1 | `HDEMBREE_USD_ROOT` | USD build with `PXR_ENABLE_EMBREE_PLUGIN=ON`; set `EMBREE_LOCATION` if Embree DLLs are not in `HDEMBREE_USD_ROOT/bin` |
|
| **hdEmbree** | Embree 4.3.3 | `HDEMBREE_USD_ROOT` (optional) | Bundled directly in `third_party/OpenUSD-v25.11` (built with `--embree`); set `HDEMBREE_USD_ROOT` only if using a separate USD build, and `EMBREE_LOCATION` if Embree DLLs aren't in its `bin/` |
|
||||||
| **hdArnold** | Arnold 7.4.0 (MtoA 5.5.0) | `HDARNOLD_ROOT`, `ARNOLD_LOCATION` | [arnold-usd](https://github.com/Autodesk/arnold-usd) install dir + MtoA root for `ai.dll` |
|
| **hdArnold** | Arnold 7.4.0 (MtoA 5.5.0) | `HDARNOLD_ROOT`, `ARNOLD_LOCATION` | [arnold-usd](https://github.com/Autodesk/arnold-usd) (tag `Arnold-7.4.5.1`, for USD 25.11's `ndr`→`sdr` API change) install dir + MtoA root for `ai.dll` |
|
||||||
| **hdCycles** | Cycles 5.2.0 | `WITH_CYCLES=ON` | Built from source under `third_party/cycles` via ExternalProject; requires MSVC |
|
| **hdCycles** | Cycles 5.2.0 | `WITH_CYCLES=ON` | Built from source under `third_party/cycles` via ExternalProject; requires MSVC |
|
||||||
|
|
||||||
All delegate paths are pre-configured in `CMakePresets.json`.
|
All delegate paths are pre-configured in `CMakePresets.json`.
|
||||||
@@ -53,7 +59,7 @@ cmake --install build --config Release
|
|||||||
|
|
||||||
The installed executable lives in `install/bin/`.
|
The installed executable lives in `install/bin/`.
|
||||||
|
|
||||||
**Presets** — `default` (Release + tests), `debug`, `release`, `no-tests`.
|
**Presets** — `default` (`Release`, `WITH_CYCLES=ON`, all render delegates), `debug` (`Debug`), `release` (`RelWithDebInfo`, installs), `no-tests` (`Release`). All presets currently build with `BUILD_TESTS=OFF`.
|
||||||
|
|
||||||
### Building with Cycles
|
### Building with Cycles
|
||||||
|
|
||||||
@@ -63,10 +69,7 @@ Cycles runtime dependencies (`embree4.dll`, `openvdb.dll`, `OpenImageDenoise.dll
|
|||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|
||||||
```powershell
|
There is currently no working test runner: `BUILD_TESTS` is `OFF` in every preset. The two test executables (`ViewportDisplayTest`, `RendererDiagnosticTest`) have stale USD include paths and don't build — fix those before turning `BUILD_TESTS` on.
|
||||||
cmake --build build --config Release
|
|
||||||
ctest --test-dir build -C Release
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -77,16 +80,23 @@ src/
|
|||||||
│ ├── UsdStageManager — Stage open/create/save/close lifecycle
|
│ ├── UsdStageManager — Stage open/create/save/close lifecycle
|
||||||
│ ├── LayerManager — Layer stack introspection and mutation
|
│ ├── LayerManager — Layer stack introspection and mutation
|
||||||
│ ├── PropertyManager — Property read/write via edit target with undo
|
│ ├── PropertyManager — Property read/write via edit target with undo
|
||||||
|
│ ├── MaterialManager — UsdShade graph introspection + Sdr node-type/enum discovery
|
||||||
│ ├── CommandHistory — Undo/redo stack for ICommand instances
|
│ ├── CommandHistory — Undo/redo stack for ICommand instances
|
||||||
│ ├── UsdSceneRenderer — Hydra rendering + picking + overlays
|
│ ├── UsdSceneRenderer — Hydra rendering + picking + overlays + OCIO color correction
|
||||||
│ ├── ViewportCamera — Free / USD-camera-driven orbital camera
|
│ ├── ViewportCamera — Free / USD-camera-driven orbital camera
|
||||||
│ └── commands/ — ICommand subclasses for all mutable operations
|
│ └── commands/ — ICommand subclasses for all mutable operations
|
||||||
│ ├── TransformCommand — Undoable translate/rotate/scale
|
│ ├── TransformCommand — Undoable translate/rotate/scale
|
||||||
│ ├── CreatePrimCommand — Undoable prim creation (with camera orientation)
|
│ ├── CreatePrimCommand — Undoable prim creation (with camera orientation)
|
||||||
│ ├── DeletePrimCommand — Undoable prim deletion (SdfCopySpec snapshot)
|
│ ├── DeletePrimCommand — Undoable prim deletion (SdfCopySpec snapshot)
|
||||||
|
│ ├── RenamePrimCommand — Undoable prim rename
|
||||||
|
│ ├── ReparentPrimCommand — Undoable reparent via UsdNamespaceEditor
|
||||||
|
│ ├── GroupPrimsCommand — Group selected prims under a new Xform
|
||||||
│ ├── AttributeSetCommand — Type-agnostic undoable attribute writes
|
│ ├── AttributeSetCommand — Type-agnostic undoable attribute writes
|
||||||
│ ├── AddReferenceCommand — Undoable reference addition
|
│ ├── AddReferenceCommand — Undoable reference addition
|
||||||
│ ├── ReplaceReferenceCommand — Undoable reference replacement
|
│ ├── ReplaceReferenceCommand — Undoable reference replacement
|
||||||
|
│ ├── CreateShaderNodeCommand — Undoable UsdShadeShader creation (Material Editor)
|
||||||
|
│ ├── ConnectShaderAttrsCommand — Undoable shader input↔output connection
|
||||||
|
│ ├── DisconnectShaderAttrCommand — Undoable shader connection removal
|
||||||
│ └── LayerCommands — Create, remove, reorder sublayers
|
│ └── LayerCommands — Create, remove, reorder sublayers
|
||||||
├── ui/ — ImGui panels and application shell
|
├── ui/ — ImGui panels and application shell
|
||||||
│ ├── Application — App shell (owns managers/panels, docking, menus)
|
│ ├── Application — App shell (owns managers/panels, docking, menus)
|
||||||
@@ -95,13 +105,21 @@ src/
|
|||||||
│ ├── ViewportTile — Single viewport tile (camera, rendering, overlays, picking)
|
│ ├── ViewportTile — Single viewport tile (camera, rendering, overlays, picking)
|
||||||
│ ├── SceneHierarchyPanel — Prim tree browser with context menus
|
│ ├── SceneHierarchyPanel — Prim tree browser with context menus
|
||||||
│ ├── PropertyPanel — Attribute/transform editor (channel box-style)
|
│ ├── PropertyPanel — Attribute/transform editor (channel box-style)
|
||||||
│ ├── LayerPanel — Layer stack editor with modal dialogs
|
│ ├── StageEditorPanel — Layer stack editor (edit target, dirty state, drag-drop ordering)
|
||||||
|
│ ├── TimelinePanel — Transport controls, loop/bounce, frame scrub, Playblast
|
||||||
|
│ ├── CurveEditorPanel — Maya-style F-curve editor (Bezier ↔ USD time samples)
|
||||||
|
│ ├── MaterialEditorPanel — Hypershade-style UsdShade node-graph editor
|
||||||
|
│ ├── MaterialPreviewRenderer — Shader-ball preview: independent Hydra instance + scratch stage
|
||||||
|
│ ├── NodeThumbnailCache — Async texture decode + amortized shader-ball thumbnails for the node canvas
|
||||||
│ ├── TransformManipulator — ImDrawList gizmo (move/rotate/scale)
|
│ ├── TransformManipulator — ImDrawList gizmo (move/rotate/scale)
|
||||||
│ └── IconManager — SVG icon rasterization via NanoSVG
|
│ ├── IconManager — SVG icon rasterization via NanoSVG
|
||||||
|
│ └── NodeEditor/ — Vendored/patched imgui-node-editor
|
||||||
└── utils/ — Cross-cutting utilities
|
└── utils/ — Cross-cutting utilities
|
||||||
├── Logger — Thread-safe logging (file + console)
|
├── Logger — Thread-safe logging (file + console); also sinks USD TF_WARN/TF_ERROR
|
||||||
├── FileDialog — Win32 file open/save dialogs
|
├── FileDialog — Win32 file open/save dialogs
|
||||||
├── PathUtils — Exe-relative path resolution
|
├── PathUtils — Exe-relative path resolution
|
||||||
|
├── OcioConfigParser — Enumerates displays/views/colorspaces/looks from the active OCIO config
|
||||||
|
├── MovieEncoder — Streams RGBA frames to H.264/MP4 via libavcodec (Playblast)
|
||||||
└── GLExt — GLAD extension initialization
|
└── GLExt — GLAD extension initialization
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -111,6 +129,7 @@ src/
|
|||||||
- **Panel-Manager Separation** — UI panels hold pointers to core managers but contain no USD layer/prim logic
|
- **Panel-Manager Separation** — UI panels hold pointers to core managers but contain no USD layer/prim logic
|
||||||
- **Callback Wiring** — Cross-panel communication via `std::function` callbacks (e.g. viewport pick → hierarchy select → property panel update)
|
- **Callback Wiring** — Cross-panel communication via `std::function` callbacks (e.g. viewport pick → hierarchy select → property panel update)
|
||||||
- **Render-Delegate Plugability** — `UsdSceneRenderer` can switch Hydra render plugins at runtime
|
- **Render-Delegate Plugability** — `UsdSceneRenderer` can switch Hydra render plugins at runtime
|
||||||
|
- **Own Color Correction Pass** — Hydra's `HdxColorCorrectionTask` is bypassed; a file-local `ViewportColorCorrector` (inside `UsdSceneRenderer.cpp`) applies sRGB/OCIO as a fullscreen GL pass after Hydra renders linear (see `docs/adr/0001-viewport-color-correction.md`)
|
||||||
- **Gizmo as 2D Overlay** — `TransformManipulator` uses pure `ImDrawList` calls (no GL resources), same approach as ImGuizmo
|
- **Gizmo as 2D Overlay** — `TransformManipulator` uses pure `ImDrawList` calls (no GL resources), same approach as ImGuizmo
|
||||||
|
|
||||||
## Project Configuration
|
## Project Configuration
|
||||||
@@ -121,6 +140,7 @@ src/
|
|||||||
| `cmake/modules/FindImgui.cmake` | ImGui source integration |
|
| `cmake/modules/FindImgui.cmake` | ImGui source integration |
|
||||||
| `cmake/modules/FindGlad.cmake` | GLAD loader setup |
|
| `cmake/modules/FindGlad.cmake` | GLAD loader setup |
|
||||||
| `CMakePresets.json` | Build presets (VS 17 2022, x64) |
|
| `CMakePresets.json` | Build presets (VS 17 2022, x64) |
|
||||||
|
| `docs/adr/` | Architecture decision records |
|
||||||
| `AGENTS.md` | AI agent build/architecture guide |
|
| `AGENTS.md` | AI agent build/architecture guide |
|
||||||
| `.kilo/` | Kilo AI configuration (commands, agents, skills) |
|
| `.kilo/` | Kilo AI configuration (commands, agents, skills) |
|
||||||
| `openspec/changes/` | Feature proposals and implementation tracking |
|
| `openspec/changes/` | Feature proposals and implementation tracking |
|
||||||
@@ -146,15 +166,14 @@ Use Kilo's `openspec-*` skills to streamline this workflow.
|
|||||||
Before writing any OpenUSD API call, verify the API exists in the actual SDK:
|
Before writing any OpenUSD API call, verify the API exists in the actual SDK:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
findstr /r /s "FunctionName" third_party\OpenUSD_v25.05\include\
|
findstr /r /s "FunctionName" third_party\OpenUSD-v25.11\include\
|
||||||
```
|
```
|
||||||
|
|
||||||
Always build and install before running tests:
|
Always build and install before manually verifying a change:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cmake --build build --config Release
|
cmake --build build --config Release
|
||||||
cmake --install build --config Release
|
cmake --install build --config Release
|
||||||
ctest --test-dir build -C Release
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ set(OpenUSD_REQUIRED_LIBS
|
|||||||
hio
|
hio
|
||||||
ar
|
ar
|
||||||
kind
|
kind
|
||||||
ndr
|
|
||||||
sdr
|
sdr
|
||||||
python
|
python
|
||||||
cameraUtil
|
cameraUtil
|
||||||
|
|||||||
@@ -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」
|
||||||
|
——以追加的方式記錄後續進展或設計變動,而非直接覆寫、抹除本次
|
||||||
|
決策當下的紀錄。
|
||||||
|
After Width: | Height: | Size: 510 KiB |
|
After Width: | Height: | Size: 518 KiB |
@@ -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 |
@@ -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 |
@@ -1 +1,39 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c8.4-19.3 10.6-41.4 4.8-63.3c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zM373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5L373 389.9z"/></svg>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="eye-slash.svg"
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.7441406"
|
||||||
|
inkscape:cx="319.92833"
|
||||||
|
inkscape:cy="256"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1137"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
|
||||||
|
<path
|
||||||
|
d="M 45.154688,63.56677 C 37.354061,57.416274 26.02815,58.841389 19.877654,66.642017 13.727159,74.442646 15.152273,85.768558 22.952903,91.919053 L 466.98865,439.94706 c 7.80061,6.1505 19.12653,4.72539 25.27703,-3.07524 6.15049,-7.80063 4.72538,-19.12654 -3.07524,-25.27704 l -78.90636,-61.80497 c 29.7024,-30.45246 49.80401,-64.5802 59.92982,-88.80716 2.4752,-5.92547 2.4752,-12.526 0,-18.45147 -11.17589,-26.77716 -34.65278,-65.7803 -69.75561,-98.33292 -35.25284,-32.85264 -83.78174,-60.454866 -144.38662,-60.454866 -51.15412,0 -93.75756,19.726586 -126.98523,45.603666 z M 183.39082,171.87549 c 19.12653,-17.47641 44.70359,-28.12727 72.68085,-28.12727 59.6298,0 108.0087,48.37889 108.0087,108.0087 0,18.6765 -4.72539,36.22791 -13.05107,51.52914 l -28.95232,-22.65182 c 6.30051,-14.47616 7.95063,-31.05249 3.60029,-47.47882 -8.32567,-31.1275 -35.85288,-52.05419 -66.45536,-53.3293 -4.35034,-0.15001 -6.90055,4.57537 -5.55044,8.77572 1.57513,4.80038 2.4752,9.90078 2.4752,15.22622 0,7.65061 -1.80014,14.85119 -4.9504,21.22671 L 183.46581,171.95049 Z M 295.82487,352.19 c -12.30099,4.87539 -25.72707,7.57562 -39.7532,7.57562 -59.62981,0 -108.0087,-48.3789 -108.0087,-108.0087 0,-5.17542 0.37503,-10.20082 1.05009,-15.15122 L 78.382365,180.87621 c -17.101377,22.27679 -29.327363,44.47858 -36.452936,61.65497 -2.475199,5.92547 -2.475199,12.526 0,18.45147 11.175899,26.77717 34.652789,65.7803 69.755611,98.33293 35.25284,32.85264 83.78174,60.45487 144.38663,60.45487 35.85289,0 67.43042,-9.67578 94.65761,-24.37697 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:0.75006" />
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -1 +1,39 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z"/></svg>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="eye.svg"
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.7441406"
|
||||||
|
inkscape:cx="287.82083"
|
||||||
|
inkscape:cy="256"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1137"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
|
||||||
|
<path
|
||||||
|
d="m 250.94921,80.734602 c -63.04963,0 -113.53614,28.715668 -150.28908,62.893558 -36.518844,33.86576 -60.942769,74.44226 -72.491463,102.29957 -2.575046,6.16451 -2.575046,13.0313 0,19.1958 11.548694,27.85732 35.972619,68.43382 72.491463,102.29958 36.75294,34.17789 87.23945,62.89356 150.28908,62.89356 63.04962,0 113.53613,-28.71567 150.28907,-62.89356 36.51884,-33.94379 60.94277,-74.44226 72.56949,-102.29958 2.57505,-6.1645 2.57505,-13.03129 0,-19.1958 C 462.18105,218.07042 437.75712,177.49392 401.23828,143.62816 364.48534,109.45027 313.99883,80.734602 250.94921,80.734602 Z M 138.58355,255.52563 a 112.36566,112.36566 0 1 1 224.73132,0 112.36566,112.36566 0 1 1 -224.73132,0 z m 112.36566,-49.94029 c 0,27.54519 -22.39511,49.94029 -49.9403,49.94029 -5.54025,0 -10.84641,-0.93637 -15.84043,-2.57505 -4.29175,-1.40456 -9.28578,1.24851 -9.12972,5.77436 0.2341,5.38418 1.01441,10.76837 2.49702,16.15256 10.69034,39.95223 51.81305,63.67387 91.76529,52.98353 39.95224,-10.69034 63.67387,-51.81305 52.98353,-91.76529 -8.66152,-32.38316 -37.29916,-54.15401 -69.13609,-55.48055 -4.52584,-0.15607 -7.17892,4.75993 -5.77435,9.12971 1.63866,4.99403 2.57505,10.30019 2.57505,15.84044 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:0.780317" />
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 863 B After Width: | Height: | Size: 2.4 KiB |
@@ -1 +1,39 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM216 232l0 48 48 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-48 0 0 48c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-48-48 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l48 0 0-48c0-13.3 10.7-24 24-24s24 10.7 24 24z"/></svg>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 64 64"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="file-plus.svg"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="13.953125"
|
||||||
|
inkscape:cx="30.351624"
|
||||||
|
inkscape:cy="32.788354"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1137"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
|
||||||
|
<path
|
||||||
|
d="m 18.366884,5.8051512 c -3.568558,0 -6.469907,2.9085663 -6.469907,6.4860018 v 38.916014 c 0,3.577435 2.901349,6.486002 6.469907,6.486002 h 25.879632 c 3.568559,0 6.469908,-2.908567 6.469908,-6.486002 V 22.020157 H 37.776609 c -1.789335,0 -3.234954,-1.449217 -3.234954,-3.243002 V 5.8051512 Z m 19.409725,0 V 18.777155 H 50.716424 Z M 33.732916,29.316909 v 4.864502 h 4.852431 c 1.344528,0 2.426216,1.084379 2.426216,2.43225 0,1.347873 -1.081688,2.432252 -2.426216,2.432252 h -4.852431 v 4.864501 c 0,1.347872 -1.081688,2.43225 -2.426216,2.43225 -1.344527,0 -2.426215,-1.084378 -2.426215,-2.43225 v -4.864501 h -4.852431 c -1.344528,0 -2.426215,-1.084379 -2.426215,-2.432252 0,-1.347871 1.081687,-2.43225 2.426215,-2.43225 h 4.852431 v -4.864502 c 0,-1.347872 1.081688,-2.43225 2.426215,-2.43225 1.344528,0 2.426216,1.084378 2.426216,2.43225 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:0.101217" />
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 634 B After Width: | Height: | Size: 2.1 KiB |
@@ -1 +1,37 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M0 32C0 14.3 14.3 0 32 0L160 0c17.7 0 32 14.3 32 32l0 384c0 53-43 96-96 96s-96-43-96-96L0 32zM223.6 425.9c.3-3.3 .4-6.6 .4-9.9l0-262 75.4-75.4c12.5-12.5 32.8-12.5 45.3 0l90.5 90.5c12.5 12.5 12.5 32.8 0 45.3L223.6 425.9zM182.8 512l192-192L480 320c17.7 0 32 14.3 32 32l0 128c0 17.7-14.3 32-32 32l-297.2 0zM128 64L64 64l0 64 64 0 0-64zM64 192l0 64 64 0 0-64-64 0zM96 440a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"/></svg>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="swatchbook.svg"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.7441406"
|
||||||
|
inkscape:cx="255.71333"
|
||||||
|
inkscape:cy="256"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1137"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
|
||||||
|
<path
|
||||||
|
d="m 36.12094,73.137738 c 0,-14.449385 12.282308,-26.12318 27.484883,-26.12318 H 173.54535 c 15.20258,0 27.48489,11.673795 27.48489,26.12318 V 386.6159 c 0,43.26652 -36.93281,78.36954 -82.45465,78.36954 -45.521839,0 -82.45465,-35.10302 -82.45465,-78.36954 z M 228.17156,394.69776 c 0.25766,-2.69395 0.34356,-5.38791 0.34356,-8.08186 V 172.73237 l 64.76125,-61.55275 c 10.73629,-10.20437 28.172,-10.20437 38.90828,0 l 77.7307,73.87962 c 10.73629,10.20436 10.73629,26.77626 0,36.98062 z m -35.04323,70.28768 164.9093,-156.73908 h 90.35655 c 15.20257,0 27.48488,11.6738 27.48488,26.12318 v 104.49273 c 0,14.44938 -12.28231,26.12317 -27.48488,26.12317 z M 146.06047,99.260915 H 91.090705 V 151.50728 H 146.06047 Z M 91.090705,203.75364 V 256 h 54.969765 v -52.24636 z m 27.484885,202.45465 a 20.613662,19.592386 0 1 0 0,-39.18477 20.613662,19.592386 0 1 0 0,39.18477 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:0.837356" />
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 693 B After Width: | Height: | Size: 2.1 KiB |
@@ -138,6 +138,7 @@ ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& material
|
|||||||
if (!materialPrim.IsValid()) return snapshot;
|
if (!materialPrim.IsValid()) return snapshot;
|
||||||
|
|
||||||
static const pxr::TfToken kUiPositionKey("uiPosition");
|
static const pxr::TfToken kUiPositionKey("uiPosition");
|
||||||
|
static const pxr::TfToken kUiPinDisplayModeKey("uiPinDisplayMode");
|
||||||
|
|
||||||
// Seed with every shader under the material, recursing into nested node
|
// Seed with every shader under the material, recursing into nested node
|
||||||
// graphs (usdMtlx nests a material's nodes inside UsdShadeNodeGraph
|
// graphs (usdMtlx nests a material's nodes inside UsdShadeNodeGraph
|
||||||
@@ -179,6 +180,13 @@ ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& material
|
|||||||
node.hasAuthoredPosition = true;
|
node.hasAuthoredPosition = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pxr::VtValue pinModeValue = child.GetCustomDataByKey(kUiPinDisplayModeKey);
|
||||||
|
if (pinModeValue.IsHolding<int>()) {
|
||||||
|
int raw = pinModeValue.UncheckedGet<int>();
|
||||||
|
if (raw >= static_cast<int>(PinDisplayMode::All) && raw <= static_cast<int>(PinDisplayMode::Collapsed))
|
||||||
|
node.pinDisplayMode = static_cast<PinDisplayMode>(raw);
|
||||||
|
}
|
||||||
|
|
||||||
// Prefer the full Sdr-defined pin set (so unauthored pins can still be
|
// Prefer the full Sdr-defined pin set (so unauthored pins can still be
|
||||||
// dragged to create a connection); fall back to authored attributes
|
// dragged to create a connection); fall back to authored attributes
|
||||||
// only for shader types the registry doesn't know about.
|
// only for shader types the registry doesn't know about.
|
||||||
@@ -187,8 +195,12 @@ ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& material
|
|||||||
if (sdrNode) {
|
if (sdrNode) {
|
||||||
node.category = DeriveCategory(sdrNode);
|
node.category = DeriveCategory(sdrNode);
|
||||||
for (const pxr::TfToken& inputName : sdrNode->GetShaderInputNames()) {
|
for (const pxr::TfToken& inputName : sdrNode->GetShaderInputNames()) {
|
||||||
if (auto* prop = sdrNode->GetShaderInput(inputName))
|
if (auto* prop = sdrNode->GetShaderInput(inputName)) {
|
||||||
node.inputs.push_back({inputName.GetString(), prop->GetTypeAsSdfType().GetSdfType()});
|
ShaderPinInfo pin{inputName.GetString(), prop->GetTypeAsSdfType().GetSdfType()};
|
||||||
|
for (const auto& option : prop->GetOptions())
|
||||||
|
pin.allowedValues.push_back(option.first.GetString());
|
||||||
|
node.inputs.push_back(std::move(pin));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const pxr::TfToken& outputName : sdrNode->GetShaderOutputNames()) {
|
for (const pxr::TfToken& outputName : sdrNode->GetShaderOutputNames()) {
|
||||||
if (auto* prop = sdrNode->GetShaderOutput(outputName))
|
if (auto* prop = sdrNode->GetShaderOutput(outputName))
|
||||||
|
|||||||
@@ -31,6 +31,21 @@ struct ShaderNodeTypeInfo {
|
|||||||
struct ShaderPinInfo {
|
struct ShaderPinInfo {
|
||||||
std::string name;
|
std::string name;
|
||||||
pxr::SdfValueTypeName typeName;
|
pxr::SdfValueTypeName typeName;
|
||||||
|
/// Sdr-declared allowed values (e.g. UsdUVTexture's sourceColorSpace:
|
||||||
|
/// "raw"/"sRGB"/"auto"), empty when the property isn't enum-constrained.
|
||||||
|
/// Drives a dropdown instead of a free-text field in the property panel.
|
||||||
|
std::vector<std::string> allowedValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Hypershade-style pin display mode for a node in the graph canvas.
|
||||||
|
/// All: every Sdr-defined pin shown (today's default). Connected: only
|
||||||
|
/// linked pins shown, the rest collapsed behind a single connectable nub per
|
||||||
|
/// side. Collapsed: every pin — linked or not — collapses behind that one
|
||||||
|
/// nub per side, so existing connections visually converge on it too.
|
||||||
|
enum class PinDisplayMode {
|
||||||
|
All = 0,
|
||||||
|
Connected = 1,
|
||||||
|
Collapsed = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// One UsdShadeShader prim read back from an existing material network.
|
/// One UsdShadeShader prim read back from an existing material network.
|
||||||
@@ -48,6 +63,9 @@ struct ShaderGraphNode {
|
|||||||
/// False when no uiPosition custom data is authored (typical for networks
|
/// False when no uiPosition custom data is authored (typical for networks
|
||||||
/// referenced from .mtlx) — the editor auto-lays such nodes out instead.
|
/// referenced from .mtlx) — the editor auto-lays such nodes out instead.
|
||||||
bool hasAuthoredPosition = false;
|
bool hasAuthoredPosition = false;
|
||||||
|
/// Pin display mode, read from customData when the "keep graph node view
|
||||||
|
/// settings in USD" preference authored it; All when unauthored.
|
||||||
|
PinDisplayMode pinDisplayMode = PinDisplayMode::All;
|
||||||
std::vector<ShaderPinInfo> inputs;
|
std::vector<ShaderPinInfo> inputs;
|
||||||
std::vector<ShaderPinInfo> outputs;
|
std::vector<ShaderPinInfo> outputs;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -174,7 +174,7 @@ UsdSceneRenderer::UsdSceneRenderer()
|
|||||||
, m_aaEnabled(false)
|
, m_aaEnabled(false)
|
||||||
, m_backgroundColor(0.15f, 0.15f, 0.15f)
|
, m_backgroundColor(0.15f, 0.15f, 0.15f)
|
||||||
, m_shadingMode(ShadingMode::SmoothShaded)
|
, m_shadingMode(ShadingMode::SmoothShaded)
|
||||||
, m_cullStyle(CullStyle::BackUnlessDoubleSided)
|
, m_cullStyle(CullStyle::Nothing) // matches usdview; see ViewportTileSettings::cullStyle
|
||||||
, m_colorCorrectionMode(ColorCorrectionMode::sRGB)
|
, m_colorCorrectionMode(ColorCorrectionMode::sRGB)
|
||||||
, m_ambientLightOnly(true)
|
, m_ambientLightOnly(true)
|
||||||
, m_domeLightEnabled(false)
|
, m_domeLightEnabled(false)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "ConnectShaderAttrsCommand.h"
|
#include "ConnectShaderAttrsCommand.h"
|
||||||
#include "../../utils/Logger.h"
|
#include "../../utils/Logger.h"
|
||||||
|
#include <pxr/usd/usd/editContext.h>
|
||||||
#include <pxr/usd/usdShade/shader.h>
|
#include <pxr/usd/usdShade/shader.h>
|
||||||
#include <pxr/usd/usdShade/connectableAPI.h>
|
#include <pxr/usd/usdShade/connectableAPI.h>
|
||||||
#include <pxr/base/tf/token.h>
|
#include <pxr/base/tf/token.h>
|
||||||
@@ -12,7 +13,8 @@ ConnectShaderAttrsCommand::ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
|
|||||||
const pxr::SdfValueTypeName& destType,
|
const pxr::SdfValueTypeName& destType,
|
||||||
const pxr::SdfPath& sourceNode,
|
const pxr::SdfPath& sourceNode,
|
||||||
const std::string& sourceOutput,
|
const std::string& sourceOutput,
|
||||||
const pxr::SdfValueTypeName& sourceType)
|
const pxr::SdfValueTypeName& sourceType,
|
||||||
|
pxr::SdfLayerHandle editLayer)
|
||||||
: m_stage(stage)
|
: m_stage(stage)
|
||||||
, m_destNode(destNode)
|
, m_destNode(destNode)
|
||||||
, m_destInput(destInput)
|
, m_destInput(destInput)
|
||||||
@@ -20,6 +22,7 @@ ConnectShaderAttrsCommand::ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
|
|||||||
, m_sourceNode(sourceNode)
|
, m_sourceNode(sourceNode)
|
||||||
, m_sourceOutput(sourceOutput)
|
, m_sourceOutput(sourceOutput)
|
||||||
, m_sourceType(sourceType)
|
, m_sourceType(sourceType)
|
||||||
|
, m_editLayer(editLayer)
|
||||||
, m_description("Connect " + sourceNode.GetName() + "." + sourceOutput +
|
, m_description("Connect " + sourceNode.GetName() + "." + sourceOutput +
|
||||||
" -> " + destNode.GetName() + "." + destInput)
|
" -> " + destNode.GetName() + "." + destInput)
|
||||||
{
|
{
|
||||||
@@ -50,9 +53,16 @@ void ConnectShaderAttrsCommand::Execute() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_editLayer) {
|
||||||
|
pxr::UsdEditContext ec(m_stage, m_editLayer);
|
||||||
pxr::UsdShadeInput destInput = destShader.CreateInput(pxr::TfToken(m_destInput), m_destType);
|
pxr::UsdShadeInput destInput = destShader.CreateInput(pxr::TfToken(m_destInput), m_destType);
|
||||||
pxr::UsdShadeOutput sourceOutput = sourceShader.CreateOutput(pxr::TfToken(m_sourceOutput), m_sourceType);
|
pxr::UsdShadeOutput sourceOutput = sourceShader.CreateOutput(pxr::TfToken(m_sourceOutput), m_sourceType);
|
||||||
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, sourceOutput);
|
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, sourceOutput);
|
||||||
|
} else {
|
||||||
|
pxr::UsdShadeInput destInput = destShader.CreateInput(pxr::TfToken(m_destInput), m_destType);
|
||||||
|
pxr::UsdShadeOutput sourceOutput = sourceShader.CreateOutput(pxr::TfToken(m_sourceOutput), m_sourceType);
|
||||||
|
pxr::UsdShadeConnectableAPI::ConnectToSource(destInput, sourceOutput);
|
||||||
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
LOG_ERROR(std::string("ConnectShaderAttrsCommand::Execute error: ") + e.what());
|
LOG_ERROR(std::string("ConnectShaderAttrsCommand::Execute error: ") + e.what());
|
||||||
}
|
}
|
||||||
@@ -66,6 +76,7 @@ void ConnectShaderAttrsCommand::Undo() {
|
|||||||
pxr::UsdShadeInput destInput = destShader.GetInput(pxr::TfToken(m_destInput));
|
pxr::UsdShadeInput destInput = destShader.GetInput(pxr::TfToken(m_destInput));
|
||||||
if (!destInput) return;
|
if (!destInput) return;
|
||||||
|
|
||||||
|
auto doUndo = [&]() {
|
||||||
if (m_hadPriorConnection) {
|
if (m_hadPriorConnection) {
|
||||||
pxr::UsdShadeShader priorSourceShader(m_stage->GetPrimAtPath(m_priorSourceNode));
|
pxr::UsdShadeShader priorSourceShader(m_stage->GetPrimAtPath(m_priorSourceNode));
|
||||||
pxr::UsdShadeOutput priorSourceOutput = priorSourceShader.GetOutput(pxr::TfToken(m_priorSourceOutput));
|
pxr::UsdShadeOutput priorSourceOutput = priorSourceShader.GetOutput(pxr::TfToken(m_priorSourceOutput));
|
||||||
@@ -74,6 +85,14 @@ void ConnectShaderAttrsCommand::Undo() {
|
|||||||
} else {
|
} else {
|
||||||
pxr::UsdShadeConnectableAPI::DisconnectSource(destInput);
|
pxr::UsdShadeConnectableAPI::DisconnectSource(destInput);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (m_editLayer) {
|
||||||
|
pxr::UsdEditContext ec(m_stage, m_editLayer);
|
||||||
|
doUndo();
|
||||||
|
} else {
|
||||||
|
doUndo();
|
||||||
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
LOG_ERROR(std::string("ConnectShaderAttrsCommand::Undo error: ") + e.what());
|
LOG_ERROR(std::string("ConnectShaderAttrsCommand::Undo error: ") + e.what());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <pxr/usd/usd/stage.h>
|
#include <pxr/usd/usd/stage.h>
|
||||||
#include <pxr/usd/sdf/path.h>
|
#include <pxr/usd/sdf/path.h>
|
||||||
#include <pxr/usd/sdf/valueTypeName.h>
|
#include <pxr/usd/sdf/valueTypeName.h>
|
||||||
|
#include <pxr/usd/sdf/layer.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace UsdLayerManager {
|
namespace UsdLayerManager {
|
||||||
@@ -14,13 +15,19 @@ namespace UsdLayerManager {
|
|||||||
/// Undo can restore it exactly rather than merely disconnecting.
|
/// Undo can restore it exactly rather than merely disconnecting.
|
||||||
class ConnectShaderAttrsCommand : public ICommand {
|
class ConnectShaderAttrsCommand : public ICommand {
|
||||||
public:
|
public:
|
||||||
|
/// editLayer: layer to author into (mirrors TransformCommand's pattern).
|
||||||
|
/// Captured at construction time so Undo() targets the same layer as
|
||||||
|
/// Execute() even if the stage's ambient edit target changes in between
|
||||||
|
/// (e.g. a render-layer switch) — pass null/empty to fall back to
|
||||||
|
/// whatever the ambient edit target is at call time (today's behavior).
|
||||||
ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
|
ConnectShaderAttrsCommand(pxr::UsdStageRefPtr stage,
|
||||||
const pxr::SdfPath& destNode,
|
const pxr::SdfPath& destNode,
|
||||||
const std::string& destInput,
|
const std::string& destInput,
|
||||||
const pxr::SdfValueTypeName& destType,
|
const pxr::SdfValueTypeName& destType,
|
||||||
const pxr::SdfPath& sourceNode,
|
const pxr::SdfPath& sourceNode,
|
||||||
const std::string& sourceOutput,
|
const std::string& sourceOutput,
|
||||||
const pxr::SdfValueTypeName& sourceType);
|
const pxr::SdfValueTypeName& sourceType,
|
||||||
|
pxr::SdfLayerHandle editLayer = pxr::SdfLayerHandle());
|
||||||
|
|
||||||
void Execute() override;
|
void Execute() override;
|
||||||
void Undo() override;
|
void Undo() override;
|
||||||
@@ -34,6 +41,7 @@ private:
|
|||||||
pxr::SdfPath m_sourceNode;
|
pxr::SdfPath m_sourceNode;
|
||||||
std::string m_sourceOutput;
|
std::string m_sourceOutput;
|
||||||
pxr::SdfValueTypeName m_sourceType;
|
pxr::SdfValueTypeName m_sourceType;
|
||||||
|
pxr::SdfLayerHandle m_editLayer;
|
||||||
std::string m_description;
|
std::string m_description;
|
||||||
|
|
||||||
bool m_hadPriorConnection = false;
|
bool m_hadPriorConnection = false;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "DisconnectShaderAttrCommand.h"
|
#include "DisconnectShaderAttrCommand.h"
|
||||||
#include "../../utils/Logger.h"
|
#include "../../utils/Logger.h"
|
||||||
|
#include <pxr/usd/usd/editContext.h>
|
||||||
#include <pxr/usd/usdShade/shader.h>
|
#include <pxr/usd/usdShade/shader.h>
|
||||||
#include <pxr/usd/usdShade/connectableAPI.h>
|
#include <pxr/usd/usdShade/connectableAPI.h>
|
||||||
#include <pxr/base/tf/token.h>
|
#include <pxr/base/tf/token.h>
|
||||||
@@ -8,10 +9,12 @@ namespace UsdLayerManager {
|
|||||||
|
|
||||||
DisconnectShaderAttrCommand::DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage,
|
DisconnectShaderAttrCommand::DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage,
|
||||||
const pxr::SdfPath& destNode,
|
const pxr::SdfPath& destNode,
|
||||||
const std::string& destInput)
|
const std::string& destInput,
|
||||||
|
pxr::SdfLayerHandle editLayer)
|
||||||
: m_stage(stage)
|
: m_stage(stage)
|
||||||
, m_destNode(destNode)
|
, m_destNode(destNode)
|
||||||
, m_destInput(destInput)
|
, m_destInput(destInput)
|
||||||
|
, m_editLayer(editLayer)
|
||||||
, m_description("Disconnect " + destNode.GetName() + "." + destInput)
|
, m_description("Disconnect " + destNode.GetName() + "." + destInput)
|
||||||
{
|
{
|
||||||
if (!stage) return;
|
if (!stage) return;
|
||||||
@@ -38,7 +41,13 @@ void DisconnectShaderAttrCommand::Execute() {
|
|||||||
if (!destShader) return;
|
if (!destShader) return;
|
||||||
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput));
|
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput));
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
|
if (m_editLayer) {
|
||||||
|
pxr::UsdEditContext ec(m_stage, m_editLayer);
|
||||||
pxr::UsdShadeConnectableAPI::DisconnectSource(input);
|
pxr::UsdShadeConnectableAPI::DisconnectSource(input);
|
||||||
|
} else {
|
||||||
|
pxr::UsdShadeConnectableAPI::DisconnectSource(input);
|
||||||
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
LOG_ERROR(std::string("DisconnectShaderAttrCommand::Execute error: ") + e.what());
|
LOG_ERROR(std::string("DisconnectShaderAttrCommand::Execute error: ") + e.what());
|
||||||
}
|
}
|
||||||
@@ -53,8 +62,14 @@ void DisconnectShaderAttrCommand::Undo() {
|
|||||||
|
|
||||||
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput));
|
pxr::UsdShadeInput input = destShader.GetInput(pxr::TfToken(m_destInput));
|
||||||
pxr::UsdShadeOutput output = sourceShader.GetOutput(pxr::TfToken(m_sourceOutput));
|
pxr::UsdShadeOutput output = sourceShader.GetOutput(pxr::TfToken(m_sourceOutput));
|
||||||
if (input && output)
|
if (!input || !output) return;
|
||||||
|
|
||||||
|
if (m_editLayer) {
|
||||||
|
pxr::UsdEditContext ec(m_stage, m_editLayer);
|
||||||
pxr::UsdShadeConnectableAPI::ConnectToSource(input, output);
|
pxr::UsdShadeConnectableAPI::ConnectToSource(input, output);
|
||||||
|
} else {
|
||||||
|
pxr::UsdShadeConnectableAPI::ConnectToSource(input, output);
|
||||||
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
LOG_ERROR(std::string("DisconnectShaderAttrCommand::Undo error: ") + e.what());
|
LOG_ERROR(std::string("DisconnectShaderAttrCommand::Undo error: ") + e.what());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "../CommandHistory.h"
|
#include "../CommandHistory.h"
|
||||||
#include <pxr/usd/usd/stage.h>
|
#include <pxr/usd/usd/stage.h>
|
||||||
#include <pxr/usd/sdf/path.h>
|
#include <pxr/usd/sdf/path.h>
|
||||||
|
#include <pxr/usd/sdf/layer.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace UsdLayerManager {
|
namespace UsdLayerManager {
|
||||||
@@ -11,9 +12,13 @@ namespace UsdLayerManager {
|
|||||||
/// source at construction time so Undo can restore it.
|
/// source at construction time so Undo can restore it.
|
||||||
class DisconnectShaderAttrCommand : public ICommand {
|
class DisconnectShaderAttrCommand : public ICommand {
|
||||||
public:
|
public:
|
||||||
|
/// editLayer: see ConnectShaderAttrsCommand's constructor doc — same
|
||||||
|
/// capture-at-construction-time pattern, null/empty falls back to the
|
||||||
|
/// ambient edit target at call time (today's behavior).
|
||||||
DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage,
|
DisconnectShaderAttrCommand(pxr::UsdStageRefPtr stage,
|
||||||
const pxr::SdfPath& destNode,
|
const pxr::SdfPath& destNode,
|
||||||
const std::string& destInput);
|
const std::string& destInput,
|
||||||
|
pxr::SdfLayerHandle editLayer = pxr::SdfLayerHandle());
|
||||||
|
|
||||||
void Execute() override;
|
void Execute() override;
|
||||||
void Undo() override;
|
void Undo() override;
|
||||||
@@ -23,6 +28,7 @@ private:
|
|||||||
pxr::UsdStageRefPtr m_stage;
|
pxr::UsdStageRefPtr m_stage;
|
||||||
pxr::SdfPath m_destNode;
|
pxr::SdfPath m_destNode;
|
||||||
std::string m_destInput;
|
std::string m_destInput;
|
||||||
|
pxr::SdfLayerHandle m_editLayer;
|
||||||
std::string m_description;
|
std::string m_description;
|
||||||
|
|
||||||
bool m_hadConnection = false;
|
bool m_hadConnection = false;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -19,6 +19,45 @@ static void SetUsdPluginPath() {
|
|||||||
std::string pluginPath = exeDir + "\\usd";
|
std::string pluginPath = exeDir + "\\usd";
|
||||||
SetEnvironmentVariableA("PXR_PLUGINPATH_NAME", pluginPath.c_str());
|
SetEnvironmentVariableA("PXR_PLUGINPATH_NAME", pluginPath.c_str());
|
||||||
|
|
||||||
|
// --- MaterialX / Arnold OSL include path -------------------------------
|
||||||
|
// hdArnold compiles MaterialX shaders by generating OSL whose first line is
|
||||||
|
// #include "mx_funcs.h"
|
||||||
|
// and compiling it as an "osl" node's `code` parameter. Without an include
|
||||||
|
// path that compile fails with:
|
||||||
|
// [osl] error: <buffer>:2:10: fatal error: 'mx_funcs.h' file not found
|
||||||
|
//
|
||||||
|
// The knob is Arnold's options.osl_includepath, which hdArnold exposes as the
|
||||||
|
// HDARNOLD_osl_includepath env setting (render_delegate/config.cpp).
|
||||||
|
//
|
||||||
|
// THIS MUST BE SET BEFORE ANY PLUGIN DLL IS LOADED. TF_DEFINE_ENV_SETTING
|
||||||
|
// registers a TF_REGISTRY_FUNCTION that eagerly calls TfGetEnvSetting() and
|
||||||
|
// caches the value permanently when hdArnold.dll loads -- which the
|
||||||
|
// LoadLibraryA pre-flight below does. Setting it later (e.g. from
|
||||||
|
// Application::Initialize) is silently ignored.
|
||||||
|
//
|
||||||
|
// Set via both the CRT (_putenv_s -> getenv) and the Win32 block
|
||||||
|
// (SetEnvironmentVariableA) since the two are not kept in sync on Windows.
|
||||||
|
{
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
const std::string mtlxLibs = exeDir + "\\libraries";
|
||||||
|
const std::string mtlxOslInclude = mtlxLibs + "\\stdlib\\genosl\\include";
|
||||||
|
|
||||||
|
if (fs::exists(mtlxOslInclude)) {
|
||||||
|
if (!getenv("HDARNOLD_osl_includepath")) {
|
||||||
|
_putenv_s("HDARNOLD_osl_includepath", mtlxOslInclude.c_str());
|
||||||
|
SetEnvironmentVariableA("HDARNOLD_osl_includepath",
|
||||||
|
mtlxOslInclude.c_str());
|
||||||
|
}
|
||||||
|
// Nodedef lookup (MATERIALX_NODE_DEFINITIONS). Separate knob from
|
||||||
|
// the OSL include path above -- it does NOT fix the include error.
|
||||||
|
if (!getenv("PXR_MTLX_STDLIB_SEARCH_PATHS")) {
|
||||||
|
_putenv_s("PXR_MTLX_STDLIB_SEARCH_PATHS", mtlxLibs.c_str());
|
||||||
|
SetEnvironmentVariableA("PXR_MTLX_STDLIB_SEARCH_PATHS",
|
||||||
|
mtlxLibs.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> pluginPaths;
|
std::vector<std::string> pluginPaths;
|
||||||
WIN32_FIND_DATAA findData;
|
WIN32_FIND_DATAA findData;
|
||||||
std::string searchPattern = pluginPath + "\\*";
|
std::string searchPattern = pluginPath + "\\*";
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ Application::Application()
|
|||||||
: m_showDemoWindow(false)
|
: m_showDemoWindow(false)
|
||||||
, m_showStageInfo(true)
|
, m_showStageInfo(true)
|
||||||
, m_showStageEditor(true)
|
, m_showStageEditor(true)
|
||||||
|
, m_showRenderLayerPanel(true)
|
||||||
, m_showSceneHierarchy(true)
|
, m_showSceneHierarchy(true)
|
||||||
, m_showViewport(true)
|
, m_showViewport(true)
|
||||||
, m_showPropertyPanel(true)
|
, m_showPropertyPanel(true)
|
||||||
@@ -67,15 +68,23 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
|
|||||||
// Create managers
|
// Create managers
|
||||||
m_stageManager = std::make_unique<UsdStageManager>();
|
m_stageManager = std::make_unique<UsdStageManager>();
|
||||||
m_layerManager = std::make_unique<LayerManager>();
|
m_layerManager = std::make_unique<LayerManager>();
|
||||||
|
m_renderLayerManager = std::make_unique<RenderLayerManager>();
|
||||||
|
m_renderLayerManager->SetLayerManager(m_layerManager.get());
|
||||||
m_propertyManager = std::make_unique<PropertyManager>();
|
m_propertyManager = std::make_unique<PropertyManager>();
|
||||||
m_propertyManager->SetCommandHistory(&m_commandHistory);
|
m_propertyManager->SetCommandHistory(&m_commandHistory);
|
||||||
m_stageEditorPanel = std::make_unique<StageEditorPanel>();
|
m_stageEditorPanel = std::make_unique<StageEditorPanel>();
|
||||||
m_stageEditorPanel->SetLayerManager(m_layerManager.get());
|
m_stageEditorPanel->SetLayerManager(m_layerManager.get());
|
||||||
m_stageEditorPanel->SetCommandHistory(&m_commandHistory);
|
m_stageEditorPanel->SetCommandHistory(&m_commandHistory);
|
||||||
|
m_stageEditorPanel->SetRenderLayerManager(m_renderLayerManager.get());
|
||||||
m_sceneHierarchyPanel = std::make_unique<SceneHierarchyPanel>();
|
m_sceneHierarchyPanel = std::make_unique<SceneHierarchyPanel>();
|
||||||
m_sceneHierarchyPanel->SetPropertyManager(m_propertyManager.get());
|
m_sceneHierarchyPanel->SetPropertyManager(m_propertyManager.get());
|
||||||
m_sceneHierarchyPanel->SetCommandHistory(&m_commandHistory);
|
m_sceneHierarchyPanel->SetCommandHistory(&m_commandHistory);
|
||||||
m_sceneHierarchyPanel->SetLayerManager(m_layerManager.get());
|
m_sceneHierarchyPanel->SetLayerManager(m_layerManager.get());
|
||||||
|
m_sceneHierarchyPanel->SetRenderLayerManager(m_renderLayerManager.get());
|
||||||
|
m_renderLayerPanel = std::make_unique<RenderLayerPanel>();
|
||||||
|
m_renderLayerPanel->SetRenderLayerManager(m_renderLayerManager.get());
|
||||||
|
m_renderLayerPanel->SetCommandHistory(&m_commandHistory);
|
||||||
|
m_renderLayerPanel->SetSceneHierarchyPanel(m_sceneHierarchyPanel.get());
|
||||||
m_viewportPanel = std::make_unique<ViewportPanel>();
|
m_viewportPanel = std::make_unique<ViewportPanel>();
|
||||||
m_viewportPanel->SetCommandHistory(&m_commandHistory);
|
m_viewportPanel->SetCommandHistory(&m_commandHistory);
|
||||||
m_propertyPanel = std::make_unique<PropertyPanel>();
|
m_propertyPanel = std::make_unique<PropertyPanel>();
|
||||||
@@ -116,6 +125,7 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
|
|||||||
m_timelinePanel->SetIconManager(m_iconManager.get());
|
m_timelinePanel->SetIconManager(m_iconManager.get());
|
||||||
m_stageEditorPanel->SetIconManager(m_iconManager.get());
|
m_stageEditorPanel->SetIconManager(m_iconManager.get());
|
||||||
m_materialEditorPanel->SetIconManager(m_iconManager.get());
|
m_materialEditorPanel->SetIconManager(m_iconManager.get());
|
||||||
|
m_renderLayerPanel->SetIconManager(m_iconManager.get());
|
||||||
|
|
||||||
m_sceneHierarchyPanel->SetOnPrimSelected(
|
m_sceneHierarchyPanel->SetOnPrimSelected(
|
||||||
[this](const std::string& path) {
|
[this](const std::string& path) {
|
||||||
@@ -177,6 +187,18 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: HDARNOLD_osl_includepath / PXR_MTLX_STDLIB_SEARCH_PATHS are set in
|
||||||
|
// main.cpp, NOT here. They must be in the environment before the plugin
|
||||||
|
// DLLs are loaded, because TF_DEFINE_ENV_SETTING caches the value when
|
||||||
|
// hdArnold.dll registers its settings. Setting them at this point is too
|
||||||
|
// late and is silently ignored.
|
||||||
|
if (const char* oslInc = getenv("HDARNOLD_osl_includepath")) {
|
||||||
|
LOG_INFO(std::string("Arnold OSL include path: ") + oslInc);
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("HDARNOLD_osl_includepath not set — Arnold MaterialX "
|
||||||
|
"shaders will fail to compile ('mx_funcs.h' not found)");
|
||||||
|
}
|
||||||
|
|
||||||
// Load per-viewport settings and global preferences from AppData.
|
// Load per-viewport settings and global preferences from AppData.
|
||||||
if (const char* appData = getenv("APPDATA")) {
|
if (const char* appData = getenv("APPDATA")) {
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
@@ -186,6 +208,7 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
|
|||||||
m_prefsPath = (dir / "preferences.ini").string();
|
m_prefsPath = (dir / "preferences.ini").string();
|
||||||
m_viewportPanel->LoadSettings(m_viewportSettingsPath);
|
m_viewportPanel->LoadSettings(m_viewportSettingsPath);
|
||||||
LoadPreferences();
|
LoadPreferences();
|
||||||
|
ValidateOcioPreferences();
|
||||||
// Apply pref delegate only to tiles that have no saved delegate.
|
// Apply pref delegate only to tiles that have no saved delegate.
|
||||||
m_viewportPanel->ApplyDefaultDelegate(m_prefs.renderDelegate);
|
m_viewportPanel->ApplyDefaultDelegate(m_prefs.renderDelegate);
|
||||||
// Color correction is always global — apply to all tiles.
|
// Color correction is always global — apply to all tiles.
|
||||||
@@ -223,10 +246,12 @@ void Application::Shutdown() {
|
|||||||
m_sceneHierarchyPanel.reset();
|
m_sceneHierarchyPanel.reset();
|
||||||
m_propertyPanel.reset();
|
m_propertyPanel.reset();
|
||||||
m_stageEditorPanel.reset();
|
m_stageEditorPanel.reset();
|
||||||
|
m_renderLayerPanel.reset();
|
||||||
// Owns the shader-ball preview's Hydra engine + GL draw target — must be
|
// Owns the shader-ball preview's Hydra engine + GL draw target — must be
|
||||||
// destroyed while the GL context still exists, like m_viewportPanel.
|
// destroyed while the GL context still exists, like m_viewportPanel.
|
||||||
m_materialEditorPanel.reset();
|
m_materialEditorPanel.reset();
|
||||||
m_propertyManager.reset();
|
m_propertyManager.reset();
|
||||||
|
m_renderLayerManager.reset();
|
||||||
m_layerManager.reset();
|
m_layerManager.reset();
|
||||||
|
|
||||||
if (m_iconManager) {
|
if (m_iconManager) {
|
||||||
@@ -251,8 +276,12 @@ void Application::RefreshManagers() {
|
|||||||
if (m_stageManager->HasStage()) {
|
if (m_stageManager->HasStage()) {
|
||||||
auto stage = m_stageManager->GetStage();
|
auto stage = m_stageManager->GetStage();
|
||||||
m_layerManager->SetStage(stage);
|
m_layerManager->SetStage(stage);
|
||||||
|
// After LayerManager (ActivateRenderLayer routes edit-target changes
|
||||||
|
// through it) but before RestorePersistedActiveLayer needs it.
|
||||||
|
m_renderLayerManager->SetStage(stage);
|
||||||
m_propertyManager->SetStage(stage);
|
m_propertyManager->SetStage(stage);
|
||||||
m_sceneHierarchyPanel->SetStage(stage);
|
m_sceneHierarchyPanel->SetStage(stage);
|
||||||
|
m_renderLayerPanel->SetStage(stage);
|
||||||
m_viewportPanel->SetStage(stage);
|
m_viewportPanel->SetStage(stage);
|
||||||
m_viewportPanel->FrameScene();
|
m_viewportPanel->FrameScene();
|
||||||
m_propertyPanel->SetStage(stage);
|
m_propertyPanel->SetStage(stage);
|
||||||
@@ -260,10 +289,16 @@ void Application::RefreshManagers() {
|
|||||||
m_curveEditorPanel->SetStage(stage);
|
m_curveEditorPanel->SetStage(stage);
|
||||||
m_materialManager->SetStage(stage);
|
m_materialManager->SetStage(stage);
|
||||||
m_materialEditorPanel->SetStage(stage);
|
m_materialEditorPanel->SetStage(stage);
|
||||||
|
// Re-applies whichever render layer was active last time this stage
|
||||||
|
// was saved (mute state itself isn't serialized — see
|
||||||
|
// RenderLayerManager::RestorePersistedActiveLayer).
|
||||||
|
m_renderLayerManager->RestorePersistedActiveLayer();
|
||||||
} else {
|
} else {
|
||||||
m_layerManager->SetStage(nullptr);
|
m_layerManager->SetStage(nullptr);
|
||||||
|
m_renderLayerManager->SetStage(nullptr);
|
||||||
m_propertyManager->SetStage(nullptr);
|
m_propertyManager->SetStage(nullptr);
|
||||||
m_sceneHierarchyPanel->SetStage(nullptr);
|
m_sceneHierarchyPanel->SetStage(nullptr);
|
||||||
|
m_renderLayerPanel->SetStage(nullptr);
|
||||||
m_viewportPanel->SetStage(nullptr);
|
m_viewportPanel->SetStage(nullptr);
|
||||||
m_propertyPanel->SetStage(nullptr);
|
m_propertyPanel->SetStage(nullptr);
|
||||||
m_timelinePanel->SetStage(nullptr);
|
m_timelinePanel->SetStage(nullptr);
|
||||||
@@ -315,6 +350,12 @@ void Application::RenderUI() {
|
|||||||
ImGui::End();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_showRenderLayerPanel) {
|
||||||
|
ImGui::Begin("Render Layers", &m_showRenderLayerPanel, ImGuiWindowFlags_NoCollapse);
|
||||||
|
m_renderLayerPanel->Render();
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
if (m_showViewport) {
|
if (m_showViewport) {
|
||||||
m_viewportPanel->Render(&m_showViewport);
|
m_viewportPanel->Render(&m_showViewport);
|
||||||
}
|
}
|
||||||
@@ -429,11 +470,85 @@ void Application::LoadPreferences()
|
|||||||
m_prefs.materialBrowserWidth = [&]{ try { return std::stof(val); } catch(...){ return 220.0f; } }();
|
m_prefs.materialBrowserWidth = [&]{ try { return std::stof(val); } catch(...){ return 220.0f; } }();
|
||||||
else if (key == "MaterialPreviewWidth")
|
else if (key == "MaterialPreviewWidth")
|
||||||
m_prefs.materialPreviewWidth = [&]{ try { return std::stof(val); } catch(...){ return 320.0f; } }();
|
m_prefs.materialPreviewWidth = [&]{ try { return std::stof(val); } catch(...){ return 320.0f; } }();
|
||||||
|
else if (key == "KeepGraphNodeViewSettingsInUsd")
|
||||||
|
m_prefs.keepGraphNodeViewSettingsInUsd = (val == "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_materialEditorPanel)
|
if (m_materialEditorPanel) {
|
||||||
m_materialEditorPanel->SetColumnWidths(m_prefs.materialBrowserWidth,
|
m_materialEditorPanel->SetColumnWidths(m_prefs.materialBrowserWidth,
|
||||||
m_prefs.materialPreviewWidth);
|
m_prefs.materialPreviewWidth);
|
||||||
|
m_materialEditorPanel->SetKeepGraphNodeViewSettingsInUsd(m_prefs.keepGraphNodeViewSettingsInUsd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::ValidateOcioPreferences()
|
||||||
|
{
|
||||||
|
// A persisted display/view that doesn't exist in the active OCIO config
|
||||||
|
// makes HdxColorCorrectionTask throw ("Display 'x' not found") every frame
|
||||||
|
// and silently skip correction, which reads as a washed-out viewport --
|
||||||
|
// worst on delegates like hdEmbree whose output depends on the transform.
|
||||||
|
// Note display and view are easy to invert: in the bundled ACES 1.2 config
|
||||||
|
// the only display is "ACES" and "sRGB" is one of its views.
|
||||||
|
const OcioConfig& cfg = GetCurrentOcioConfig();
|
||||||
|
if (!cfg.valid) {
|
||||||
|
// No $OCIO / unreadable config -- nothing to validate against. Leave
|
||||||
|
// the prefs alone rather than clobbering values that may be correct
|
||||||
|
// for a config supplied later.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto contains = [](const std::vector<std::string>& v, const std::string& s) {
|
||||||
|
return std::find(v.begin(), v.end(), s) != v.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- display ---
|
||||||
|
if (m_prefs.ocioDisplay.empty() || !contains(cfg.displays, m_prefs.ocioDisplay)) {
|
||||||
|
if (!m_prefs.ocioDisplay.empty()) {
|
||||||
|
LOG_WARNING("OCIO display '" + m_prefs.ocioDisplay
|
||||||
|
+ "' not in config; falling back to '"
|
||||||
|
+ cfg.defaultDisplay + "'");
|
||||||
|
}
|
||||||
|
m_prefs.ocioDisplay = cfg.defaultDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- view (must belong to the display resolved above) ---
|
||||||
|
auto viewsIt = cfg.views.find(m_prefs.ocioDisplay);
|
||||||
|
const std::vector<std::string>* views =
|
||||||
|
(viewsIt != cfg.views.end()) ? &viewsIt->second : nullptr;
|
||||||
|
|
||||||
|
if (views && !views->empty()) {
|
||||||
|
if (m_prefs.ocioView.empty() || !contains(*views, m_prefs.ocioView)) {
|
||||||
|
// Prefer the config default when it's valid for this display,
|
||||||
|
// otherwise take the display's first view.
|
||||||
|
const std::string fallback =
|
||||||
|
contains(*views, cfg.defaultView) ? cfg.defaultView : views->front();
|
||||||
|
if (!m_prefs.ocioView.empty()) {
|
||||||
|
LOG_WARNING("OCIO view '" + m_prefs.ocioView
|
||||||
|
+ "' not valid for display '" + m_prefs.ocioDisplay
|
||||||
|
+ "'; falling back to '" + fallback + "'");
|
||||||
|
}
|
||||||
|
m_prefs.ocioView = fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- color space (same failure mode if it names a missing space) ---
|
||||||
|
if (!m_prefs.ocioColorSpace.empty()
|
||||||
|
&& !contains(cfg.colorSpaces, m_prefs.ocioColorSpace)) {
|
||||||
|
LOG_WARNING("OCIO color space '" + m_prefs.ocioColorSpace
|
||||||
|
+ "' not in config; clearing it");
|
||||||
|
m_prefs.ocioColorSpace.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- look ---
|
||||||
|
if (!m_prefs.ocioLook.empty()
|
||||||
|
&& !contains(cfg.looks, m_prefs.ocioLook)) {
|
||||||
|
LOG_WARNING("OCIO look '" + m_prefs.ocioLook
|
||||||
|
+ "' not in config; clearing it");
|
||||||
|
m_prefs.ocioLook.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("OCIO display/view: '" + m_prefs.ocioDisplay + "' / '"
|
||||||
|
+ m_prefs.ocioView + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::SavePreferences()
|
void Application::SavePreferences()
|
||||||
@@ -455,6 +570,7 @@ void Application::SavePreferences()
|
|||||||
f << "OcioLook=" << m_prefs.ocioLook << "\n";
|
f << "OcioLook=" << m_prefs.ocioLook << "\n";
|
||||||
f << "MaterialBrowserWidth=" << m_prefs.materialBrowserWidth << "\n";
|
f << "MaterialBrowserWidth=" << m_prefs.materialBrowserWidth << "\n";
|
||||||
f << "MaterialPreviewWidth=" << m_prefs.materialPreviewWidth << "\n";
|
f << "MaterialPreviewWidth=" << m_prefs.materialPreviewWidth << "\n";
|
||||||
|
f << "KeepGraphNodeViewSettingsInUsd=" << (m_prefs.keepGraphNodeViewSettingsInUsd ? 1 : 0) << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::ApplyPrefsToAllViewports()
|
void Application::ApplyPrefsToAllViewports()
|
||||||
@@ -505,6 +621,23 @@ void Application::RenderPreferencesDialog()
|
|||||||
ImGui::EndTabItem();
|
ImGui::EndTabItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Material ────────────────────────────────────────────────────────
|
||||||
|
if (ImGui::BeginTabItem("Material")) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (ImGui::Checkbox("Keep graph node view settings in USD customData",
|
||||||
|
&m_prefs.keepGraphNodeViewSettingsInUsd)) {
|
||||||
|
if (m_materialEditorPanel)
|
||||||
|
m_materialEditorPanel->SetKeepGraphNodeViewSettingsInUsd(
|
||||||
|
m_prefs.keepGraphNodeViewSettingsInUsd);
|
||||||
|
}
|
||||||
|
ImGui::TextWrapped(
|
||||||
|
"When enabled, each node's pin display mode (all pins vs. "
|
||||||
|
"connected only) and any manually revealed pins are saved into "
|
||||||
|
"the material file and restored when it's reopened. When "
|
||||||
|
"disabled, this is a session-only view setting.");
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Viewport ────────────────────────────────────────────────────────
|
// ── Viewport ────────────────────────────────────────────────────────
|
||||||
if (ImGui::BeginTabItem("Viewport")) {
|
if (ImGui::BeginTabItem("Viewport")) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -743,6 +876,7 @@ void Application::RenderMenuBar() {
|
|||||||
|
|
||||||
if (ImGui::BeginMenu("View")) {
|
if (ImGui::BeginMenu("View")) {
|
||||||
ImGui::MenuItem("Stage Editor", nullptr, &m_showStageEditor);
|
ImGui::MenuItem("Stage Editor", nullptr, &m_showStageEditor);
|
||||||
|
ImGui::MenuItem("Render Layers", nullptr, &m_showRenderLayerPanel);
|
||||||
ImGui::MenuItem("Scene Hierarchy", nullptr, &m_showSceneHierarchy);
|
ImGui::MenuItem("Scene Hierarchy", nullptr, &m_showSceneHierarchy);
|
||||||
ImGui::MenuItem("Viewport", nullptr, &m_showViewport);
|
ImGui::MenuItem("Viewport", nullptr, &m_showViewport);
|
||||||
ImGui::MenuItem("Property Panel", nullptr, &m_showPropertyPanel);
|
ImGui::MenuItem("Property Panel", nullptr, &m_showPropertyPanel);
|
||||||
@@ -838,7 +972,9 @@ void Application::SaveUsdFile() {
|
|||||||
|
|
||||||
if (!m_stageManager->SaveStage()) {
|
if (!m_stageManager->SaveStage()) {
|
||||||
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
|
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
SaveDirtyRenderLayers();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::SaveUsdFileAs() {
|
void Application::SaveUsdFileAs() {
|
||||||
@@ -857,6 +993,11 @@ void Application::SaveUsdFileAs() {
|
|||||||
if (!m_stageManager->SaveStageAs(filePath)) {
|
if (!m_stageManager->SaveStageAs(filePath)) {
|
||||||
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
|
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
|
||||||
} else {
|
} else {
|
||||||
|
// m_renderLayerManager's layer refs are identifier-keyed SdfLayer
|
||||||
|
// objects, independent of which UsdStage currently references
|
||||||
|
// them — safe to save before the stage swap RefreshManagers()
|
||||||
|
// below performs.
|
||||||
|
SaveDirtyRenderLayers();
|
||||||
// SaveStageAs reopens m_stageManager's stage from the new file path.
|
// SaveStageAs reopens m_stageManager's stage from the new file path.
|
||||||
// RefreshManagers syncs m_layerManager (and others) to that new stage;
|
// RefreshManagers syncs m_layerManager (and others) to that new stage;
|
||||||
// without this, subsequent sublayer edits go to the old (now stale) stage
|
// without this, subsequent sublayer edits go to the old (now stale) stage
|
||||||
@@ -866,6 +1007,15 @@ void Application::SaveUsdFileAs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Application::SaveDirtyRenderLayers() {
|
||||||
|
if (!m_renderLayerManager) return;
|
||||||
|
for (const auto& info : m_renderLayerManager->GetRenderLayers()) {
|
||||||
|
if (info.isDefault || !info.layer) continue;
|
||||||
|
if (info.layer->IsDirty())
|
||||||
|
info.layer->Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Application::CloseUsdFile() {
|
void Application::CloseUsdFile() {
|
||||||
m_stageManager->CloseStage();
|
m_stageManager->CloseStage();
|
||||||
// Re-create a fresh default stage so the app is always in an editable state.
|
// Re-create a fresh default stage so the app is always in an editable state.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "IconManager.h"
|
#include "IconManager.h"
|
||||||
#include "StageEditorPanel.h"
|
#include "StageEditorPanel.h"
|
||||||
#include "SceneHierarchyPanel.h"
|
#include "SceneHierarchyPanel.h"
|
||||||
|
#include "RenderLayerPanel.h"
|
||||||
#include "ViewportPanel.h"
|
#include "ViewportPanel.h"
|
||||||
#include "PropertyPanel.h"
|
#include "PropertyPanel.h"
|
||||||
#include "TimelinePanel.h"
|
#include "TimelinePanel.h"
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
#include "MaterialEditorPanel.h"
|
#include "MaterialEditorPanel.h"
|
||||||
#include "../core/UsdStageManager.h"
|
#include "../core/UsdStageManager.h"
|
||||||
#include "../core/LayerManager.h"
|
#include "../core/LayerManager.h"
|
||||||
|
#include "../core/RenderLayerManager.h"
|
||||||
#include "../core/PropertyManager.h"
|
#include "../core/PropertyManager.h"
|
||||||
#include "../core/MaterialManager.h"
|
#include "../core/MaterialManager.h"
|
||||||
#include "../core/CommandHistory.h"
|
#include "../core/CommandHistory.h"
|
||||||
@@ -30,6 +32,11 @@ struct AppPreferences {
|
|||||||
std::string ocioLook;
|
std::string ocioLook;
|
||||||
float materialBrowserWidth = 220.0f; ///< Material Editor left-column width
|
float materialBrowserWidth = 220.0f; ///< Material Editor left-column width
|
||||||
float materialPreviewWidth = 320.0f; ///< Material Editor right-column width
|
float materialPreviewWidth = 320.0f; ///< Material Editor right-column width
|
||||||
|
/// When true, per-node pin display mode (Hypershade-style "show all" vs
|
||||||
|
/// "show connected only") and revealed hidden pins are authored into USD
|
||||||
|
/// customData so they survive reopening the material; when false, that
|
||||||
|
/// state is session-only.
|
||||||
|
bool keepGraphNodeViewSettingsInUsd = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Application {
|
class Application {
|
||||||
@@ -60,12 +67,22 @@ private:
|
|||||||
void AddReferenceToStage();
|
void AddReferenceToStage();
|
||||||
void CreatePrimOnStage(const std::string& typeName);
|
void CreatePrimOnStage(const std::string& typeName);
|
||||||
|
|
||||||
|
/// Explicitly saves every render-layer SdfLayer, since at most one is
|
||||||
|
/// ever in the stage's composition (unmuted) at a time and
|
||||||
|
/// UsdStage::Save() only walks layers currently in composition — the
|
||||||
|
/// rest would otherwise be silently dropped on save.
|
||||||
|
void SaveDirtyRenderLayers();
|
||||||
|
|
||||||
// Playblast
|
// Playblast
|
||||||
void CapturePlayblastFrame();
|
void CapturePlayblastFrame();
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
void LoadPreferences();
|
void LoadPreferences();
|
||||||
void SavePreferences();
|
void SavePreferences();
|
||||||
|
/// Reconcile persisted OCIO display/view names against the active config.
|
||||||
|
/// Falls back to the config defaults when a name doesn't exist, so a stale
|
||||||
|
/// or mistyped preference can't silently disable color correction.
|
||||||
|
void ValidateOcioPreferences();
|
||||||
void ApplyPrefsToAllViewports();
|
void ApplyPrefsToAllViewports();
|
||||||
void RenderPreferencesDialog();
|
void RenderPreferencesDialog();
|
||||||
|
|
||||||
@@ -73,11 +90,13 @@ private:
|
|||||||
std::unique_ptr<IconManager> m_iconManager;
|
std::unique_ptr<IconManager> m_iconManager;
|
||||||
std::unique_ptr<UsdStageManager> m_stageManager;
|
std::unique_ptr<UsdStageManager> m_stageManager;
|
||||||
std::unique_ptr<LayerManager> m_layerManager;
|
std::unique_ptr<LayerManager> m_layerManager;
|
||||||
|
std::unique_ptr<RenderLayerManager> m_renderLayerManager;
|
||||||
std::unique_ptr<PropertyManager> m_propertyManager;
|
std::unique_ptr<PropertyManager> m_propertyManager;
|
||||||
std::unique_ptr<MaterialManager> m_materialManager;
|
std::unique_ptr<MaterialManager> m_materialManager;
|
||||||
CommandHistory m_commandHistory;
|
CommandHistory m_commandHistory;
|
||||||
std::unique_ptr<StageEditorPanel> m_stageEditorPanel;
|
std::unique_ptr<StageEditorPanel> m_stageEditorPanel;
|
||||||
std::unique_ptr<SceneHierarchyPanel> m_sceneHierarchyPanel;
|
std::unique_ptr<SceneHierarchyPanel> m_sceneHierarchyPanel;
|
||||||
|
std::unique_ptr<RenderLayerPanel> m_renderLayerPanel;
|
||||||
std::unique_ptr<ViewportPanel> m_viewportPanel;
|
std::unique_ptr<ViewportPanel> m_viewportPanel;
|
||||||
std::unique_ptr<PropertyPanel> m_propertyPanel;
|
std::unique_ptr<PropertyPanel> m_propertyPanel;
|
||||||
std::unique_ptr<TimelinePanel> m_timelinePanel;
|
std::unique_ptr<TimelinePanel> m_timelinePanel;
|
||||||
@@ -86,6 +105,7 @@ private:
|
|||||||
bool m_showDemoWindow;
|
bool m_showDemoWindow;
|
||||||
bool m_showStageInfo;
|
bool m_showStageInfo;
|
||||||
bool m_showStageEditor;
|
bool m_showStageEditor;
|
||||||
|
bool m_showRenderLayerPanel;
|
||||||
bool m_showSceneHierarchy;
|
bool m_showSceneHierarchy;
|
||||||
bool m_showViewport;
|
bool m_showViewport;
|
||||||
bool m_showPropertyPanel;
|
bool m_showPropertyPanel;
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ static const char* IconFilename(Icon icon) {
|
|||||||
case Icon::FilePlus: return "file-plus.svg";
|
case Icon::FilePlus: return "file-plus.svg";
|
||||||
case Icon::Pen: return "pen.svg";
|
case Icon::Pen: return "pen.svg";
|
||||||
case Icon::Settings: return "gear.svg";
|
case Icon::Settings: return "gear.svg";
|
||||||
|
case Icon::ChevronUp: return "chevron-up.svg";
|
||||||
|
case Icon::ChevronDown: return "chevron-down.svg";
|
||||||
default: return nullptr;
|
default: return nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +79,7 @@ static constexpr Icon kAllIcons[] = {
|
|||||||
Icon::SkipBack, Icon::StepBack, Icon::PlayBack, Icon::Play, Icon::Pause,
|
Icon::SkipBack, Icon::StepBack, Icon::PlayBack, Icon::Play, Icon::Pause,
|
||||||
Icon::StepForward, Icon::SkipEnd, Icon::Loop, Icon::Bounce,
|
Icon::StepForward, Icon::SkipEnd, Icon::Loop, Icon::Bounce,
|
||||||
Icon::Refresh, Icon::FilePlus, Icon::Pen, Icon::Settings,
|
Icon::Refresh, Icon::FilePlus, Icon::Pen, Icon::Settings,
|
||||||
|
Icon::ChevronUp, Icon::ChevronDown,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ enum class Icon {
|
|||||||
FilePlus, // create new file
|
FilePlus, // create new file
|
||||||
Pen, // edit / edit target
|
Pen, // edit / edit target
|
||||||
Settings, // gear / viewport options
|
Settings, // gear / viewport options
|
||||||
|
// List reordering
|
||||||
|
ChevronUp, // move item up
|
||||||
|
ChevronDown, // move item down
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Loads SVG files from disk, rasterizes them with NanoSVG, uploads them as
|
/// Loads SVG files from disk, rasterizes them with NanoSVG, uploads them as
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include "../core/commands/RenamePrimCommand.h"
|
#include "../core/commands/RenamePrimCommand.h"
|
||||||
#include "../utils/Logger.h"
|
#include "../utils/Logger.h"
|
||||||
#include "../utils/FileDialog.h"
|
#include "../utils/FileDialog.h"
|
||||||
|
#include "../utils/OcioConfigParser.h"
|
||||||
|
#include <pxr/usd/usd/editContext.h>
|
||||||
#include <pxr/usd/usdShade/shader.h>
|
#include <pxr/usd/usdShade/shader.h>
|
||||||
#include <pxr/usd/usdShade/material.h>
|
#include <pxr/usd/usdShade/material.h>
|
||||||
#include <pxr/usd/usdShade/materialBindingAPI.h>
|
#include <pxr/usd/usdShade/materialBindingAPI.h>
|
||||||
@@ -45,6 +47,29 @@ uintptr_t HashId(const std::string& s) {
|
|||||||
return static_cast<uintptr_t>(std::hash<std::string>{}(s));
|
return static_cast<uintptr_t>(std::hash<std::string>{}(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draws the 3-dot pin-display-mode icon (filled-dot count = 3 - mode, so it
|
||||||
|
// visually "funnels down" as pins collapse further), centered within
|
||||||
|
// [min, max]. Shared by the per-node toggle in RenderNodeGraphCanvas (which
|
||||||
|
// sizes its button around the default dot geometry) and the toolbar's
|
||||||
|
// "set all nodes" button in RenderToolbar (which instead fits the dots into
|
||||||
|
// its fixed, already-square icon-button size via smaller dotRadius/dotSpacing).
|
||||||
|
void DrawPinDisplayModeIcon(ImDrawList* dl, const ImVec2& min, const ImVec2& max,
|
||||||
|
PinDisplayMode mode, ImU32 filledColor, ImU32 hollowColor,
|
||||||
|
float dotRadius = 3.0f, float dotSpacing = 9.0f) {
|
||||||
|
const int dotCount = 3;
|
||||||
|
const int filledDots = 3 - static_cast<int>(mode);
|
||||||
|
const float iconWidth = dotSpacing * (dotCount - 1) + dotRadius * 2.0f;
|
||||||
|
ImVec2 dotCenter(min.x + (max.x - min.x - iconWidth) * 0.5f + dotRadius,
|
||||||
|
min.y + (max.y - min.y) * 0.5f);
|
||||||
|
for (int i = 0; i < dotCount; ++i) {
|
||||||
|
if (i < filledDots)
|
||||||
|
dl->AddCircleFilled(dotCenter, dotRadius, filledColor);
|
||||||
|
else
|
||||||
|
dl->AddCircle(dotCenter, dotRadius, hollowColor, 0, 1.2f);
|
||||||
|
dotCenter.x += dotSpacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mirrors the file-local sanitizers in Application.cpp/SceneHierarchyPanel.cpp.
|
// Mirrors the file-local sanitizers in Application.cpp/SceneHierarchyPanel.cpp.
|
||||||
std::string SanitizeUsdName(const std::string& raw) {
|
std::string SanitizeUsdName(const std::string& raw) {
|
||||||
std::string result;
|
std::string result;
|
||||||
@@ -306,7 +331,7 @@ void ApplyFallbackLayout(ShaderGraphSnapshot& graph) {
|
|||||||
MaterialEditorPanel::MaterialEditorPanel() {
|
MaterialEditorPanel::MaterialEditorPanel() {
|
||||||
// Route the vendored editor's click-time hit-test dump into our log so
|
// Route the vendored editor's click-time hit-test dump into our log so
|
||||||
// it interleaves with the [NodeGraph] event trace. Disabled: uncomment to
|
// it interleaves with the [NodeGraph] event trace. Disabled: uncomment to
|
||||||
// re-enable the [ed] trace while diagnosing the drag regression.
|
// re-enable the [ed] trace when diagnosing canvas interaction issues.
|
||||||
// g_AxNodeEditorDebugLog = [](const char* msg) { LOG_INFO(std::string(msg)); };
|
// g_AxNodeEditorDebugLog = [](const char* msg) { LOG_INFO(std::string(msg)); };
|
||||||
|
|
||||||
NE::Config config;
|
NE::Config config;
|
||||||
@@ -342,6 +367,14 @@ void MaterialEditorPanel::SetColorCorrectionFromPrefs(int ccMode, const std::str
|
|||||||
const std::string& ocioColorSpace,
|
const std::string& ocioColorSpace,
|
||||||
const std::string& ocioLook) {
|
const std::string& ocioLook) {
|
||||||
m_preview.SetColorCorrection(ccMode, ocioDisplay, ocioView, ocioColorSpace, ocioLook);
|
m_preview.SetColorCorrection(ccMode, ocioDisplay, ocioView, ocioColorSpace, ocioLook);
|
||||||
|
// Cached for the per-frame NodeThumbnailCache::SyncRenderSettings() call
|
||||||
|
// in RenderNodeGraphCanvas, which keeps in-canvas node thumbnails
|
||||||
|
// matching this same color correction (see its declaration).
|
||||||
|
m_ccMode = ccMode;
|
||||||
|
m_ocioDisplay = ocioDisplay;
|
||||||
|
m_ocioView = ocioView;
|
||||||
|
m_ocioColorSpace = ocioColorSpace;
|
||||||
|
m_ocioLook = ocioLook;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MaterialEditorPanel::Render() {
|
void MaterialEditorPanel::Render() {
|
||||||
@@ -547,6 +580,18 @@ void MaterialEditorPanel::RenderSelectedNodeProperties() {
|
|||||||
RenderInputValueWidget(*node, input, prim);
|
RenderInputValueWidget(*node, input, prim);
|
||||||
ImGui::PopItemWidth();
|
ImGui::PopItemWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.typeName == pxr::SdfValueTypeNames->Asset) {
|
||||||
|
ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Color Space");
|
||||||
|
ImGui::TableSetColumnIndex(2);
|
||||||
|
ImGui::PushItemWidth(-FLT_MIN);
|
||||||
|
RenderColorSpaceWidget(*node, input, prim);
|
||||||
|
ImGui::PopItemWidth();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
ImGui::EndTable();
|
ImGui::EndTable();
|
||||||
@@ -622,12 +667,40 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
|
|||||||
std::string s;
|
std::string s;
|
||||||
if (current.IsHolding<std::string>()) s = current.UncheckedGet<std::string>();
|
if (current.IsHolding<std::string>()) s = current.UncheckedGet<std::string>();
|
||||||
else if (current.IsHolding<pxr::TfToken>()) s = current.UncheckedGet<pxr::TfToken>().GetString();
|
else if (current.IsHolding<pxr::TfToken>()) s = current.UncheckedGet<pxr::TfToken>().GetString();
|
||||||
|
|
||||||
|
if (!input.allowedValues.empty()) {
|
||||||
|
// Sdr-declared enum (e.g. UsdUVTexture's sourceColorSpace:
|
||||||
|
// raw/sRGB/auto) — a dropdown of exactly the legal values instead
|
||||||
|
// of free text, which could author anything the renderer ignores.
|
||||||
|
// A selection is one atomic edit (no drag-in-progress concept for
|
||||||
|
// a combo), so it's committed inline here — like the Asset browse
|
||||||
|
// button below — instead of through the drag/type commit
|
||||||
|
// machinery at the end of this function.
|
||||||
|
if (ImGui::BeginCombo(widgetId.c_str(), s.c_str())) {
|
||||||
|
for (const std::string& option : input.allowedValues) {
|
||||||
|
bool selected = (option == s);
|
||||||
|
if (ImGui::Selectable(option.c_str(), selected) && !selected) {
|
||||||
|
m_preEditValue = current;
|
||||||
|
m_preEditWasAuthored = authored;
|
||||||
|
pxr::UsdShadeShader shader(prim);
|
||||||
|
if (shader)
|
||||||
|
shader.CreateInput(pxr::TfToken(input.name), input.typeName)
|
||||||
|
.Set(type == tn->String ? pxr::VtValue(option) : pxr::VtValue(pxr::TfToken(option)));
|
||||||
|
CommitInputEdit(node, input);
|
||||||
|
}
|
||||||
|
if (selected) ImGui::SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
char buf[512];
|
char buf[512];
|
||||||
std::snprintf(buf, sizeof(buf), "%s", s.c_str());
|
std::snprintf(buf, sizeof(buf), "%s", s.c_str());
|
||||||
if (ImGui::InputText(widgetId.c_str(), buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) {
|
if (ImGui::InputText(widgetId.c_str(), buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||||
if (type == tn->String) newValue = std::string(buf);
|
if (type == tn->String) newValue = std::string(buf);
|
||||||
else newValue = pxr::TfToken(buf);
|
else newValue = pxr::TfToken(buf);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (type == tn->Asset) {
|
} else if (type == tn->Asset) {
|
||||||
std::string s;
|
std::string s;
|
||||||
if (current.IsHolding<pxr::SdfAssetPath>()) s = current.UncheckedGet<pxr::SdfAssetPath>().GetAssetPath();
|
if (current.IsHolding<pxr::SdfAssetPath>()) s = current.UncheckedGet<pxr::SdfAssetPath>().GetAssetPath();
|
||||||
@@ -692,6 +765,64 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
|
|||||||
CommitInputEdit(node, input);
|
CommitInputEdit(node, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MaterialEditorPanel::RenderColorSpaceWidget(const ShaderGraphNode& node, const ShaderPinInfo& input,
|
||||||
|
const pxr::UsdPrim& prim) {
|
||||||
|
if (!m_stage || !m_commandHistory) return;
|
||||||
|
pxr::UsdAttribute attr = prim.GetAttribute(pxr::TfToken("inputs:" + input.name));
|
||||||
|
if (!attr) return;
|
||||||
|
|
||||||
|
const std::string current = attr.HasColorSpace() ? attr.GetColorSpace().GetString() : std::string();
|
||||||
|
|
||||||
|
// Listed from the active OCIO config ($OCIO — the same one driving
|
||||||
|
// viewport/preview color correction) rather than a fixed raw/sRGB/auto
|
||||||
|
// set: those are only meaningful to UsdUVTexture's own sourceColorSpace
|
||||||
|
// input, while this authors the renderer-agnostic colorSpace attribute
|
||||||
|
// metadata, so the choices should span whatever color spaces are
|
||||||
|
// actually defined and thus consistently interpretable across every
|
||||||
|
// render delegate (Storm, Arnold, Cycles, ...) rather than being tied to
|
||||||
|
// one shading backend's built-in vocabulary.
|
||||||
|
const OcioConfig& ocfg = GetCurrentOcioConfig();
|
||||||
|
static const std::vector<std::string> kFallback = {"raw", "sRGB"};
|
||||||
|
const std::vector<std::string>& colorSpaces = ocfg.valid && !ocfg.colorSpaces.empty()
|
||||||
|
? ocfg.colorSpaces : kFallback;
|
||||||
|
|
||||||
|
auto commit = [&](const std::string& newValue) {
|
||||||
|
pxr::UsdStageRefPtr stage = m_stage;
|
||||||
|
pxr::SdfPath nodePath = node.path;
|
||||||
|
pxr::TfToken attrName = attr.GetName();
|
||||||
|
std::string oldValue = current;
|
||||||
|
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
|
||||||
|
"Set color space on " + node.path.GetName() + "." + input.name,
|
||||||
|
[stage, nodePath, attrName, newValue]() {
|
||||||
|
pxr::UsdAttribute a = stage->GetPrimAtPath(nodePath).GetAttribute(attrName);
|
||||||
|
if (!a) return;
|
||||||
|
if (newValue.empty()) a.ClearColorSpace();
|
||||||
|
else a.SetColorSpace(pxr::TfToken(newValue));
|
||||||
|
},
|
||||||
|
[stage, nodePath, attrName, oldValue]() {
|
||||||
|
pxr::UsdAttribute a = stage->GetPrimAtPath(nodePath).GetAttribute(attrName);
|
||||||
|
if (!a) return;
|
||||||
|
if (oldValue.empty()) a.ClearColorSpace();
|
||||||
|
else a.SetColorSpace(pxr::TfToken(oldValue));
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ImGui::BeginCombo("##colorSpace", current.empty() ? "(unset)" : current.c_str())) {
|
||||||
|
if (ImGui::Selectable("(unset)", current.empty()) && !current.empty())
|
||||||
|
commit(std::string());
|
||||||
|
if (current.empty()) ImGui::SetItemDefaultFocus();
|
||||||
|
for (const std::string& cs : colorSpaces) {
|
||||||
|
const bool selected = (cs == current);
|
||||||
|
if (ImGui::Selectable(cs.c_str(), selected) && !selected)
|
||||||
|
commit(cs);
|
||||||
|
if (selected) ImGui::SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered() && ocfg.valid)
|
||||||
|
ImGui::SetTooltip("Color spaces from the active OCIO config");
|
||||||
|
}
|
||||||
|
|
||||||
void MaterialEditorPanel::CommitInputEdit(const ShaderGraphNode& node, const ShaderPinInfo& input) {
|
void MaterialEditorPanel::CommitInputEdit(const ShaderGraphNode& node, const ShaderPinInfo& input) {
|
||||||
if (!m_stage || !m_commandHistory) return;
|
if (!m_stage || !m_commandHistory) return;
|
||||||
|
|
||||||
@@ -788,6 +919,50 @@ void MaterialEditorPanel::RenderToolbar() {
|
|||||||
ImGui::EndPopup();
|
ImGui::EndPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (iconBtn("##toggleThumbs", m_showNodeThumbnails ? Icon::Eye : Icon::EyeSlash,
|
||||||
|
m_showNodeThumbnails ? "Hide Thumbnails" : "Show Thumbnails",
|
||||||
|
m_showNodeThumbnails ? "Hide node thumbnails" : "Show node thumbnails"))
|
||||||
|
m_showNodeThumbnails = !m_showNodeThumbnails;
|
||||||
|
|
||||||
|
// "Set all nodes" pin-display broadcast: same 3-dot icon language as the
|
||||||
|
// per-node toggle (DrawPinDisplayModeIcon), but hit-tested the normal
|
||||||
|
// ImGui way — this button lives in the toolbar window, not inside the
|
||||||
|
// node canvas, so none of RenderNodeGraphCanvas's geometric hit-test
|
||||||
|
// workarounds are needed here.
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::BeginDisabled(m_materialPath.IsEmpty());
|
||||||
|
{
|
||||||
|
ImGui::PushID("##allPinMode");
|
||||||
|
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||||
|
const ImVec2 posMax(pos.x + btnH, pos.y + btnH);
|
||||||
|
const bool clicked = ImGui::InvisibleButton("##btn", ImVec2(btnH, btnH));
|
||||||
|
const bool hovered = ImGui::IsItemHovered();
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
const ImU32 bg = ImGui::GetColorU32(
|
||||||
|
hovered ? (ImGui::IsItemActive() ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered)
|
||||||
|
: ImGuiCol_Button);
|
||||||
|
dl->AddRectFilled(pos, posMax, bg, ImGui::GetStyle().FrameRounding);
|
||||||
|
// Dot geometry scaled to btnH (unlike the per-node toggle's fixed
|
||||||
|
// size) so the icon fits inside this fixed, already-square button
|
||||||
|
// instead of overflowing it.
|
||||||
|
DrawPinDisplayModeIcon(dl, pos, posMax, m_toolbarPinDisplayMode,
|
||||||
|
ImGui::GetColorU32(ImGuiCol_Text), ImGui::GetColorU32(ImGuiCol_TextDisabled),
|
||||||
|
btnH * 0.09f, btnH * 0.30f);
|
||||||
|
static const char* kTips[3] = {
|
||||||
|
"All nodes: showing all pins - click to show connected pins only on every node",
|
||||||
|
"All nodes: showing connected pins only - click to collapse every node to one pin",
|
||||||
|
"All nodes: collapsed to one pin per side - click to show all pins on every node",
|
||||||
|
};
|
||||||
|
if (hovered) ImGui::SetTooltip("%s", kTips[static_cast<int>(m_toolbarPinDisplayMode)]);
|
||||||
|
if (clicked) {
|
||||||
|
m_toolbarPinDisplayMode = static_cast<PinDisplayMode>((static_cast<int>(m_toolbarPinDisplayMode) + 1) % 3);
|
||||||
|
ApplyPinDisplayModeToAllNodes(m_toolbarPinDisplayMode);
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
if (!m_targetPrimPath.IsEmpty()) {
|
if (!m_targetPrimPath.IsEmpty()) {
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::Text("Selected: %s", m_targetPrimPath.GetText());
|
ImGui::Text("Selected: %s", m_targetPrimPath.GetText());
|
||||||
@@ -1254,25 +1429,42 @@ void MaterialEditorPanel::BindMaterialToTarget(const pxr::SdfPath& materialPath,
|
|||||||
}
|
}
|
||||||
|
|
||||||
pxr::UsdStageRefPtr stage = m_stage;
|
pxr::UsdStageRefPtr stage = m_stage;
|
||||||
auto doBind = [stage, targetPath, materialPath]() {
|
// Captured at construction time so Undo() lands in the same layer as
|
||||||
|
// Execute() even if the ambient edit target changes in between (e.g. a
|
||||||
|
// render-layer switch) — mirrors ConnectShaderAttrsCommand's pattern.
|
||||||
|
pxr::SdfLayerHandle editLayer = m_stage->GetEditTarget().GetLayer();
|
||||||
|
auto doBind = [stage, targetPath, materialPath, editLayer]() {
|
||||||
pxr::UsdPrim target = stage->GetPrimAtPath(targetPath);
|
pxr::UsdPrim target = stage->GetPrimAtPath(targetPath);
|
||||||
pxr::UsdShadeMaterial material(stage->GetPrimAtPath(materialPath));
|
pxr::UsdShadeMaterial material(stage->GetPrimAtPath(materialPath));
|
||||||
if (target && material)
|
if (!target || !material) return;
|
||||||
|
if (editLayer) {
|
||||||
|
pxr::UsdEditContext ec(stage, editLayer);
|
||||||
pxr::UsdShadeMaterialBindingAPI::Apply(target).Bind(material);
|
pxr::UsdShadeMaterialBindingAPI::Apply(target).Bind(material);
|
||||||
|
} else {
|
||||||
|
pxr::UsdShadeMaterialBindingAPI::Apply(target).Bind(material);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (m_commandHistory) {
|
if (m_commandHistory) {
|
||||||
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
|
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
|
||||||
"Bind Material",
|
"Bind Material",
|
||||||
doBind,
|
doBind,
|
||||||
[stage, targetPath, hadPriorRel, priorTargets]() {
|
[stage, targetPath, hadPriorRel, priorTargets, editLayer]() {
|
||||||
pxr::UsdPrim target = stage->GetPrimAtPath(targetPath);
|
pxr::UsdPrim target = stage->GetPrimAtPath(targetPath);
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
auto doUnbind = [&]() {
|
||||||
pxr::UsdShadeMaterialBindingAPI bindAPI(target);
|
pxr::UsdShadeMaterialBindingAPI bindAPI(target);
|
||||||
if (hadPriorRel && !priorTargets.empty())
|
if (hadPriorRel && !priorTargets.empty())
|
||||||
bindAPI.GetDirectBindingRel().SetTargets(priorTargets);
|
bindAPI.GetDirectBindingRel().SetTargets(priorTargets);
|
||||||
else
|
else
|
||||||
bindAPI.UnbindDirectBinding();
|
bindAPI.UnbindDirectBinding();
|
||||||
|
};
|
||||||
|
if (editLayer) {
|
||||||
|
pxr::UsdEditContext ec(stage, editLayer);
|
||||||
|
doUnbind();
|
||||||
|
} else {
|
||||||
|
doUnbind();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
@@ -1311,6 +1503,16 @@ void MaterialEditorPanel::SyncFromUsd() {
|
|||||||
}
|
}
|
||||||
m_graph = m_materialManager->GetShaderGraph(m_materialPath);
|
m_graph = m_materialManager->GetShaderGraph(m_materialPath);
|
||||||
ApplyFallbackLayout(m_graph);
|
ApplyFallbackLayout(m_graph);
|
||||||
|
SeedPinDisplayOverrides();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MaterialEditorPanel::SeedPinDisplayOverrides() {
|
||||||
|
for (const auto& node : m_graph.nodes) {
|
||||||
|
const std::string key = node.path.GetString();
|
||||||
|
if (m_pinDisplayOverrides.find(key) != m_pinDisplayOverrides.end())
|
||||||
|
continue;
|
||||||
|
m_pinDisplayOverrides.emplace(key, PinDisplayOverride{node.pinDisplayMode});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
||||||
@@ -1341,6 +1543,13 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
|||||||
const ImVec2 viewCenterScreen(regionPos.x + regionSize.x * 0.5f,
|
const ImVec2 viewCenterScreen(regionPos.x + regionSize.x * 0.5f,
|
||||||
regionPos.y + regionSize.y * 0.5f);
|
regionPos.y + regionSize.y * 0.5f);
|
||||||
|
|
||||||
|
// Keep node thumbnails' renderer/color-correction matching the big
|
||||||
|
// preview swatch (cheap no-op unless something actually changed) before
|
||||||
|
// pumping, so a change this frame invalidates stale entries in time to
|
||||||
|
// be picked up by the pump below.
|
||||||
|
m_thumbnails.SyncRenderSettings(m_preview.GetRendererPlugin(), m_ccMode,
|
||||||
|
m_ocioDisplay, m_ocioView, m_ocioColorSpace, m_ocioLook);
|
||||||
|
|
||||||
// Drain finished texture decodes (GL upload), render one stale material
|
// Drain finished texture decodes (GL upload), render one stale material
|
||||||
// thumbnail, and prune dead entries — GL/FBO work kept outside the
|
// thumbnail, and prune dead entries — GL/FBO work kept outside the
|
||||||
// node-editor's Begin/End so it never touches mid-draw ImGui state.
|
// node-editor's Begin/End so it never touches mid-draw ImGui state.
|
||||||
@@ -1365,6 +1574,12 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
|||||||
// One whole-graph revision hash drives every material thumbnail this frame.
|
// One whole-graph revision hash drives every material thumbnail this frame.
|
||||||
const size_t graphRevision = ComputeGraphRevision(m_stage, m_graph);
|
const size_t graphRevision = ComputeGraphRevision(m_stage, m_graph);
|
||||||
|
|
||||||
|
// Tooltip requested by a hovered in-canvas widget this frame. Shown after
|
||||||
|
// NE::End(): inside the canvas io.MousePos is in canvas space, so a
|
||||||
|
// tooltip opened there is positioned at canvas coordinates misread as
|
||||||
|
// screen coordinates — deferring restores correct placement at the cursor.
|
||||||
|
const char* pendingTooltip = nullptr;
|
||||||
|
|
||||||
for (const auto& node : m_graph.nodes) {
|
for (const auto& node : m_graph.nodes) {
|
||||||
uintptr_t nodeIdValue = HashId(node.path.GetString());
|
uintptr_t nodeIdValue = HashId(node.path.GetString());
|
||||||
NE::NodeId nodeId(nodeIdValue);
|
NE::NodeId nodeId(nodeIdValue);
|
||||||
@@ -1380,9 +1595,11 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
|||||||
// Thumbnail below the title: the source image for Texture nodes, a
|
// Thumbnail below the title: the source image for Texture nodes, a
|
||||||
// shader-ball render for Material nodes. thumb is 0 until ready; the
|
// shader-ball render for Material nodes. thumb is 0 until ready; the
|
||||||
// space is still reserved so the node doesn't jump when it appears.
|
// space is still reserved so the node doesn't jump when it appears.
|
||||||
|
// Toggled off entirely (no reserved space, no decode/render work) via
|
||||||
|
// the toolbar's thumbnail eye icon.
|
||||||
ImTextureID thumb = 0;
|
ImTextureID thumb = 0;
|
||||||
bool wantsThumb = false;
|
bool wantsThumb = false;
|
||||||
if (node.category == "Texture") {
|
if (m_showNodeThumbnails && node.category == "Texture") {
|
||||||
// Any texture node that exposes a file/asset input reserves a
|
// Any texture node that exposes a file/asset input reserves a
|
||||||
// thumbnail slot. It stays black until a valid, on-disk image is
|
// thumbnail slot. It stays black until a valid, on-disk image is
|
||||||
// decoded, so an unset or unresolvable path reads as a black chip.
|
// decoded, so an unset or unresolvable path reads as a black chip.
|
||||||
@@ -1395,7 +1612,7 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
|||||||
if (!file.empty())
|
if (!file.empty())
|
||||||
thumb = m_thumbnails.GetTextureThumbnail(node.path, file);
|
thumb = m_thumbnails.GetTextureThumbnail(node.path, file);
|
||||||
}
|
}
|
||||||
} else if (node.category == "Material") {
|
} else if (m_showNodeThumbnails && node.category == "Material") {
|
||||||
wantsThumb = true;
|
wantsThumb = true;
|
||||||
// Preview the terminal (token-typed) output as the surface; fall
|
// Preview the terminal (token-typed) output as the surface; fall
|
||||||
// back to the first output routed into diffuseColor.
|
// back to the first output routed into diffuseColor.
|
||||||
@@ -1414,18 +1631,88 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
|||||||
// ImGui fork this project doesn't use) — approximate the classic
|
// ImGui fork this project doesn't use) — approximate the classic
|
||||||
// "outputs hug the right edge" look by right-aligning each output row
|
// "outputs hug the right edge" look by right-aligning each output row
|
||||||
// within the widest row measured across the whole node.
|
// within the widest row measured across the whole node.
|
||||||
|
PinDisplayOverride& pinDisplay = GetPinDisplayOverride(node.path);
|
||||||
|
|
||||||
|
// Connected mode collapses every unconnected pin on a side into one
|
||||||
|
// connectable "more pins" nub (Maya Hypershade style), present only
|
||||||
|
// while that side actually has something hidden. Collapsed mode
|
||||||
|
// collapses every pin on a side — linked or not — into that nub
|
||||||
|
// whenever the side has any pins at all (existing links redirect to
|
||||||
|
// it too; see EffectivePinId()).
|
||||||
|
bool hasHiddenInput = false, hasHiddenOutput = false;
|
||||||
|
if (pinDisplay.mode == PinDisplayMode::Connected) {
|
||||||
|
for (const auto& input : node.inputs)
|
||||||
|
if (!IsPinLinked(node.path, input.name, false)) { hasHiddenInput = true; break; }
|
||||||
|
for (const auto& output : node.outputs)
|
||||||
|
if (!IsPinLinked(node.path, output.name, true)) { hasHiddenOutput = true; break; }
|
||||||
|
} else if (pinDisplay.mode == PinDisplayMode::Collapsed) {
|
||||||
|
hasHiddenInput = !node.inputs.empty();
|
||||||
|
hasHiddenOutput = !node.outputs.empty();
|
||||||
|
}
|
||||||
|
|
||||||
const float rowSpacing = ImGui::GetStyle().ItemSpacing.x;
|
const float rowSpacing = ImGui::GetStyle().ItemSpacing.x;
|
||||||
float contentWidth = ImGui::CalcTextSize(title.c_str()).x;
|
float contentWidth = ImGui::CalcTextSize(title.c_str()).x;
|
||||||
for (const auto& input : node.inputs)
|
for (const auto& input : node.inputs)
|
||||||
|
if (IsPinVisible(node, input.name, false))
|
||||||
contentWidth = std::max(contentWidth, pinIconSize.x + rowSpacing + ImGui::CalcTextSize(input.name.c_str()).x);
|
contentWidth = std::max(contentWidth, pinIconSize.x + rowSpacing + ImGui::CalcTextSize(input.name.c_str()).x);
|
||||||
for (const auto& output : node.outputs)
|
for (const auto& output : node.outputs)
|
||||||
|
if (IsPinVisible(node, output.name, true))
|
||||||
contentWidth = std::max(contentWidth, pinIconSize.x + rowSpacing + ImGui::CalcTextSize(output.name.c_str()).x);
|
contentWidth = std::max(contentWidth, pinIconSize.x + rowSpacing + ImGui::CalcTextSize(output.name.c_str()).x);
|
||||||
if (wantsThumb)
|
if (wantsThumb)
|
||||||
contentWidth = std::max(contentWidth, static_cast<float>(NodeThumbnailCache::kThumbPx));
|
contentWidth = std::max(contentWidth, static_cast<float>(NodeThumbnailCache::kThumbPx));
|
||||||
|
|
||||||
NE::BeginNode(nodeId);
|
NE::BeginNode(nodeId);
|
||||||
|
ImGui::PushID(node.path.GetText());
|
||||||
ImVec2 headerTop = ImGui::GetCursorScreenPos();
|
ImVec2 headerTop = ImGui::GetCursorScreenPos();
|
||||||
|
|
||||||
ImGui::TextUnformatted(title.c_str());
|
ImGui::TextUnformatted(title.c_str());
|
||||||
|
|
||||||
|
// Mode toggle, drawn and hit-tested manually. ImGui's own hover
|
||||||
|
// attribution is unreliable for widgets inside the canvas (the same
|
||||||
|
// mis-attribution the vendored editor's FindPinAt()/m_PressedNode
|
||||||
|
// patches work around for pins and node drags — the hover race is
|
||||||
|
// lost to the editor's background item), so an ImGui::SmallButton
|
||||||
|
// here never highlights or clicks on most nodes. Hit-test the rect
|
||||||
|
// geometrically instead, like those patches do. io.MousePos is in
|
||||||
|
// canvas space inside NE::Begin/End, the same space as the cursor
|
||||||
|
// position, so the comparison is direct; canvasHovered (screen-space,
|
||||||
|
// captured before NE::Begin) gates out clicks landing on other panels
|
||||||
|
// or popups, since the canvas-space transform is unclamped.
|
||||||
|
{
|
||||||
|
const ImGuiStyle& style = ImGui::GetStyle();
|
||||||
|
// Icon area sized to match the 3-dot icon's fixed geometry (see
|
||||||
|
// DrawPinDisplayModeIcon); filled-dot count visually "funnels
|
||||||
|
// down" as the node collapses further (All=3, Connected=2,
|
||||||
|
// Collapsed=1).
|
||||||
|
const ImVec2 iconSize(9.0f * 2.0f + 3.0f * 2.0f, 3.0f * 2.0f);
|
||||||
|
const ImVec2 btnMin = ImGui::GetCursorScreenPos();
|
||||||
|
const ImVec2 btnMax(btnMin.x + iconSize.x + style.FramePadding.x * 2.0f,
|
||||||
|
btnMin.y + iconSize.y + style.FramePadding.y * 2.0f);
|
||||||
|
ImGui::Dummy(ImVec2(btnMax.x - btnMin.x, btnMax.y - btnMin.y));
|
||||||
|
|
||||||
|
// IsMouseHoveringRect is pure geometry (no hover-id arbitration)
|
||||||
|
// and clips against the canvas view, so off-view parts don't hit.
|
||||||
|
const bool btnHovered = canvasHovered && ImGui::IsMouseHoveringRect(btnMin, btnMax);
|
||||||
|
const ImU32 frameColor = ImGui::GetColorU32(
|
||||||
|
btnHovered ? (ImGui::IsMouseDown(ImGuiMouseButton_Left) ? ImGuiCol_ButtonActive
|
||||||
|
: ImGuiCol_ButtonHovered)
|
||||||
|
: ImGuiCol_Button);
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
dl->AddRectFilled(btnMin, btnMax, frameColor, style.FrameRounding);
|
||||||
|
DrawPinDisplayModeIcon(dl, btnMin, btnMax, pinDisplay.mode,
|
||||||
|
ImGui::GetColorU32(ImGuiCol_Text), ImGui::GetColorU32(ImGuiCol_TextDisabled));
|
||||||
|
|
||||||
|
static const char* kTooltips[3] = {
|
||||||
|
"Showing all pins - click to show connected pins only",
|
||||||
|
"Showing connected pins only - click to collapse into one pin",
|
||||||
|
"Collapsed to one pin per side - click to show all pins",
|
||||||
|
};
|
||||||
|
if (btnHovered)
|
||||||
|
pendingTooltip = kTooltips[static_cast<int>(pinDisplay.mode)];
|
||||||
|
if (btnHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left))
|
||||||
|
CyclePinDisplayMode(node.path);
|
||||||
|
}
|
||||||
|
|
||||||
ImVec2 headerBottom = ImGui::GetItemRectMax();
|
ImVec2 headerBottom = ImGui::GetItemRectMax();
|
||||||
ImGui::Dummy(ImVec2(0.0f, 4.0f)); // breathing room between title and pins
|
ImGui::Dummy(ImVec2(0.0f, 4.0f)); // breathing room between title and pins
|
||||||
|
|
||||||
@@ -1451,6 +1738,7 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
|||||||
uintptr_t pinIdValue = HashId(node.path.GetString() + ":in:" + input.name);
|
uintptr_t pinIdValue = HashId(node.path.GetString() + ":in:" + input.name);
|
||||||
m_pinIdToInfo[pinIdValue] = PinInfo{node.path, input.name, false, input.typeName};
|
m_pinIdToInfo[pinIdValue] = PinInfo{node.path, input.name, false, input.typeName};
|
||||||
bool linked = IsPinLinked(node.path, input.name, false);
|
bool linked = IsPinLinked(node.path, input.name, false);
|
||||||
|
if (!IsPinVisible(node, input.name, false)) continue;
|
||||||
NE::BeginPin(NE::PinId(pinIdValue), NE::PinKind::Input);
|
NE::BeginPin(NE::PinId(pinIdValue), NE::PinKind::Input);
|
||||||
// Default pivot is the center of the whole icon+text row, which
|
// Default pivot is the center of the whole icon+text row, which
|
||||||
// makes links land mid-label instead of at the icon. Pin it to
|
// makes links land mid-label instead of at the icon. Pin it to
|
||||||
@@ -1466,6 +1754,7 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
|||||||
uintptr_t pinIdValue = HashId(node.path.GetString() + ":out:" + output.name);
|
uintptr_t pinIdValue = HashId(node.path.GetString() + ":out:" + output.name);
|
||||||
m_pinIdToInfo[pinIdValue] = PinInfo{node.path, output.name, true, output.typeName};
|
m_pinIdToInfo[pinIdValue] = PinInfo{node.path, output.name, true, output.typeName};
|
||||||
bool linked = IsPinLinked(node.path, output.name, true);
|
bool linked = IsPinLinked(node.path, output.name, true);
|
||||||
|
if (!IsPinVisible(node, output.name, true)) continue;
|
||||||
float rowWidth = ImGui::CalcTextSize(output.name.c_str()).x + rowSpacing + pinIconSize.x;
|
float rowWidth = ImGui::CalcTextSize(output.name.c_str()).x + rowSpacing + pinIconSize.x;
|
||||||
if (contentWidth > rowWidth)
|
if (contentWidth > rowWidth)
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (contentWidth - rowWidth));
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (contentWidth - rowWidth));
|
||||||
@@ -1479,6 +1768,38 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
|||||||
NE::EndPin();
|
NE::EndPin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collapsed connector nubs on the node's bottom row (Connected or
|
||||||
|
// Collapsed mode): input nub at the left edge, output nub hugging the
|
||||||
|
// right edge. Dropping a connection on (or dragging one out of) a
|
||||||
|
// nub opens RenderPinPickMenu() to choose which pin it wires to.
|
||||||
|
if (hasHiddenInput || hasHiddenOutput) {
|
||||||
|
ImGui::Dummy(ImVec2(0.0f, 2.0f));
|
||||||
|
const float nubRowStartX = ImGui::GetCursorPosX();
|
||||||
|
if (hasHiddenInput) {
|
||||||
|
uintptr_t metaInId = HashId(node.path.GetString() + ":metaIn");
|
||||||
|
m_pinIdToInfo[metaInId] = PinInfo{node.path, "", false, pxr::SdfValueTypeName(), true};
|
||||||
|
NE::BeginPin(NE::PinId(metaInId), NE::PinKind::Input);
|
||||||
|
NE::PinPivotAlignment(ImVec2(0.0f, 0.5f));
|
||||||
|
NE::PinPivotSize(ImVec2(0.0f, 0.0f));
|
||||||
|
ax::Widgets::Icon(pinIconSize, ax::Widgets::IconType::Circle, false, ImColor(180, 180, 180, 255));
|
||||||
|
NE::EndPin();
|
||||||
|
if (ImGui::IsItemHovered()) pendingTooltip = "Connect to a hidden input";
|
||||||
|
}
|
||||||
|
if (hasHiddenOutput) {
|
||||||
|
if (hasHiddenInput) ImGui::SameLine();
|
||||||
|
ImGui::SetCursorPosX(nubRowStartX + contentWidth - pinIconSize.x);
|
||||||
|
uintptr_t metaOutId = HashId(node.path.GetString() + ":metaOut");
|
||||||
|
m_pinIdToInfo[metaOutId] = PinInfo{node.path, "", true, pxr::SdfValueTypeName(), true};
|
||||||
|
NE::BeginPin(NE::PinId(metaOutId), NE::PinKind::Output);
|
||||||
|
NE::PinPivotAlignment(ImVec2(1.0f, 0.5f));
|
||||||
|
NE::PinPivotSize(ImVec2(0.0f, 0.0f));
|
||||||
|
ax::Widgets::Icon(pinIconSize, ax::Widgets::IconType::Circle, false, ImColor(180, 180, 180, 255));
|
||||||
|
NE::EndPin();
|
||||||
|
if (ImGui::IsItemHovered()) pendingTooltip = "Connect a hidden output";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PopID();
|
||||||
NE::EndNode();
|
NE::EndNode();
|
||||||
|
|
||||||
// Colored header strip drawn on the node's background draw list,
|
// Colored header strip drawn on the node's background draw list,
|
||||||
@@ -1498,8 +1819,9 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
|||||||
|
|
||||||
for (const auto& link : m_graph.links) {
|
for (const auto& link : m_graph.links) {
|
||||||
uintptr_t linkIdValue = HashId(link.destNode.GetString() + ":" + link.destInput);
|
uintptr_t linkIdValue = HashId(link.destNode.GetString() + ":" + link.destInput);
|
||||||
|
// The pin's own id, always populated in m_pinIdToInfo regardless of
|
||||||
|
// visibility (used here purely to color the link by the real type).
|
||||||
uintptr_t startPinId = HashId(link.sourceNode.GetString() + ":out:" + link.sourceOutput);
|
uintptr_t startPinId = HashId(link.sourceNode.GetString() + ":out:" + link.sourceOutput);
|
||||||
uintptr_t endPinId = HashId(link.destNode.GetString() + ":in:" + link.destInput);
|
|
||||||
m_linkIdToInfo[linkIdValue] = LinkInfo{link.destNode, link.destInput};
|
m_linkIdToInfo[linkIdValue] = LinkInfo{link.destNode, link.destInput};
|
||||||
|
|
||||||
ImU32 linkColor = IM_COL32(200, 200, 200, 255);
|
ImU32 linkColor = IM_COL32(200, 200, 200, 255);
|
||||||
@@ -1507,7 +1829,13 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
|||||||
if (pinIt != m_pinIdToInfo.end())
|
if (pinIt != m_pinIdToInfo.end())
|
||||||
linkColor = GetPinColor(pinIt->second.typeName);
|
linkColor = GetPinColor(pinIt->second.typeName);
|
||||||
|
|
||||||
NE::Link(NE::LinkId(linkIdValue), NE::PinId(startPinId), NE::PinId(endPinId), ImColor(linkColor));
|
// The id actually referenced by the drawn link: redirected to a
|
||||||
|
// node's collapsed meta nub when that node is in Collapsed mode
|
||||||
|
// (the real pin isn't drawn there, so its id can't be referenced).
|
||||||
|
uintptr_t linkStartPinId = EffectivePinId(link.sourceNode, link.sourceOutput, true);
|
||||||
|
uintptr_t linkEndPinId = EffectivePinId(link.destNode, link.destInput, false);
|
||||||
|
|
||||||
|
NE::Link(NE::LinkId(linkIdValue), NE::PinId(linkStartPinId), NE::PinId(linkEndPinId), ImColor(linkColor));
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleCreateAndDelete();
|
HandleCreateAndDelete();
|
||||||
@@ -1529,10 +1857,22 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
|
|||||||
RenderNodeSearchMenu(NE::ScreenToCanvas(m_pendingCreateNodePos), &m_linkDragSourcePin);
|
RenderNodeSearchMenu(NE::ScreenToCanvas(m_pendingCreateNodePos), &m_linkDragSourcePin);
|
||||||
ImGui::EndPopup();
|
ImGui::EndPopup();
|
||||||
}
|
}
|
||||||
|
if (m_openPinPickMenu) {
|
||||||
|
ImGui::OpenPopup("PinPickMenu");
|
||||||
|
m_openPinPickMenu = false;
|
||||||
|
}
|
||||||
|
if (ImGui::BeginPopup("PinPickMenu")) {
|
||||||
|
RenderPinPickMenu();
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
NE::Resume();
|
NE::Resume();
|
||||||
|
|
||||||
NE::End();
|
NE::End();
|
||||||
|
|
||||||
|
// Deferred from the node loop — see pendingTooltip's declaration.
|
||||||
|
if (pendingTooltip)
|
||||||
|
ImGui::SetTooltip("%s", pendingTooltip);
|
||||||
|
|
||||||
// --- Debug tracing of node interaction events. Edge-triggered so each
|
// --- Debug tracing of node interaction events. Edge-triggered so each
|
||||||
// event logs once, not per frame. Event order in the editor is:
|
// event logs once, not per frame. Event order in the editor is:
|
||||||
// LMB press -> node becomes active (and is brought to front) -> drag
|
// LMB press -> node becomes active (and is brought to front) -> drag
|
||||||
@@ -1723,8 +2063,20 @@ void MaterialEditorPanel::HandleCreateAndDelete() {
|
|||||||
const PinInfo& b = endIt->second;
|
const PinInfo& b = endIt->second;
|
||||||
const PinInfo& outPin = a.isOutput ? a : b;
|
const PinInfo& outPin = a.isOutput ? a : b;
|
||||||
const PinInfo& inPin = a.isOutput ? b : a;
|
const PinInfo& inPin = a.isOutput ? b : a;
|
||||||
|
if (outPin.isMeta || inPin.isMeta) {
|
||||||
|
// Dropped on (or dragged from) a collapsed "more pins"
|
||||||
|
// nub: defer to RenderPinPickMenu() to choose which
|
||||||
|
// actual pin it wires to, instead of connecting now.
|
||||||
|
const PinInfo& metaSide = outPin.isMeta ? outPin : inPin;
|
||||||
|
const PinInfo& otherSide = outPin.isMeta ? inPin : outPin;
|
||||||
|
m_pendingPinPick.nodePath = metaSide.nodePath;
|
||||||
|
m_pendingPinPick.isOutput = metaSide.isOutput;
|
||||||
|
m_pendingPinPick.otherPin = otherSide;
|
||||||
|
m_openPinPickMenu = true;
|
||||||
|
} else {
|
||||||
CreateConnection(inPin, outPin);
|
CreateConnection(inPin, outPin);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
NE::RejectNewItem();
|
NE::RejectNewItem();
|
||||||
}
|
}
|
||||||
@@ -1735,10 +2087,13 @@ void MaterialEditorPanel::HandleCreateAndDelete() {
|
|||||||
// and pop the create-node menu (deferred to the suspended block below,
|
// and pop the create-node menu (deferred to the suspended block below,
|
||||||
// since OpenPopup must not run mid-editor); the chosen node is wired to
|
// since OpenPopup must not run mid-editor); the chosen node is wired to
|
||||||
// this pin.
|
// this pin.
|
||||||
|
// Dragging from a collapsed meta pin onto empty canvas is not
|
||||||
|
// supported (there's no single concrete pin/type to auto-wire the
|
||||||
|
// new node to) — left un-accepted here so the library cancels it.
|
||||||
NE::PinId newNodePinId;
|
NE::PinId newNodePinId;
|
||||||
if (NE::QueryNewNode(&newNodePinId)) {
|
if (NE::QueryNewNode(&newNodePinId)) {
|
||||||
auto it = m_pinIdToInfo.find(newNodePinId.Get());
|
auto it = m_pinIdToInfo.find(newNodePinId.Get());
|
||||||
if (it != m_pinIdToInfo.end() && NE::AcceptNewItem()) {
|
if (it != m_pinIdToInfo.end() && !it->second.isMeta && NE::AcceptNewItem()) {
|
||||||
m_linkDragSourcePin = it->second;
|
m_linkDragSourcePin = it->second;
|
||||||
m_openLinkDragMenu = true;
|
m_openLinkDragMenu = true;
|
||||||
m_pendingCreateNodePos = ImGui::GetMousePos();
|
m_pendingCreateNodePos = ImGui::GetMousePos();
|
||||||
@@ -1779,6 +2134,87 @@ bool MaterialEditorPanel::IsPinLinked(const pxr::SdfPath& nodePath, const std::s
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MaterialEditorPanel::PinDisplayOverride& MaterialEditorPanel::GetPinDisplayOverride(const pxr::SdfPath& nodePath) {
|
||||||
|
return m_pinDisplayOverrides[nodePath.GetString()];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaterialEditorPanel::IsPinVisible(const ShaderGraphNode& node, const std::string& pinName, bool isOutput) const {
|
||||||
|
auto it = m_pinDisplayOverrides.find(node.path.GetString());
|
||||||
|
PinDisplayMode mode = (it != m_pinDisplayOverrides.end()) ? it->second.mode : PinDisplayMode::All;
|
||||||
|
switch (mode) {
|
||||||
|
case PinDisplayMode::All: return true;
|
||||||
|
case PinDisplayMode::Connected: return IsPinLinked(node.path, pinName, isOutput);
|
||||||
|
case PinDisplayMode::Collapsed: return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MaterialEditorPanel::CyclePinDisplayMode(const pxr::SdfPath& nodePath) {
|
||||||
|
PinDisplayOverride& ov = GetPinDisplayOverride(nodePath);
|
||||||
|
switch (ov.mode) {
|
||||||
|
case PinDisplayMode::All: ov.mode = PinDisplayMode::Connected; break;
|
||||||
|
case PinDisplayMode::Connected: ov.mode = PinDisplayMode::Collapsed; break;
|
||||||
|
case PinDisplayMode::Collapsed: ov.mode = PinDisplayMode::All; break;
|
||||||
|
}
|
||||||
|
PersistPinDisplayState(nodePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MaterialEditorPanel::ApplyPinDisplayModeToAllNodes(PinDisplayMode mode) {
|
||||||
|
for (const auto& node : m_graph.nodes) {
|
||||||
|
PinDisplayOverride& ov = GetPinDisplayOverride(node.path);
|
||||||
|
if (ov.mode != mode) {
|
||||||
|
ov.mode = mode;
|
||||||
|
PersistPinDisplayState(node.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uintptr_t MaterialEditorPanel::EffectivePinId(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const {
|
||||||
|
auto it = m_pinDisplayOverrides.find(nodePath.GetString());
|
||||||
|
if (it != m_pinDisplayOverrides.end() && it->second.mode == PinDisplayMode::Collapsed)
|
||||||
|
return HashId(nodePath.GetString() + (isOutput ? ":metaOut" : ":metaIn"));
|
||||||
|
return HashId(nodePath.GetString() + (isOutput ? ":out:" : ":in:") + pinName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MaterialEditorPanel::RenderPinPickMenu() {
|
||||||
|
const ShaderGraphNode* node = nullptr;
|
||||||
|
for (const auto& n : m_graph.nodes)
|
||||||
|
if (n.path == m_pendingPinPick.nodePath) { node = &n; break; }
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
const bool isOutput = m_pendingPinPick.isOutput;
|
||||||
|
const std::vector<ShaderPinInfo>& pins = isOutput ? node->outputs : node->inputs;
|
||||||
|
|
||||||
|
ImGui::TextDisabled(isOutput ? "Connect output" : "Connect input");
|
||||||
|
ImGui::Separator();
|
||||||
|
for (const auto& pin : pins) {
|
||||||
|
if (IsPinLinked(node->path, pin.name, isOutput)) continue; // already wired elsewhere
|
||||||
|
if (ImGui::MenuItem(pin.name.c_str())) {
|
||||||
|
CompletePinPick(PinInfo{node->path, pin.name, isOutput, pin.typeName, false});
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MaterialEditorPanel::CompletePinPick(const PinInfo& chosen) {
|
||||||
|
if (m_pendingPinPick.otherPin.isMeta) {
|
||||||
|
// Dragged directly between two collapsed nubs: this pick resolved
|
||||||
|
// one side to a real pin, now chain into a second pick for the far
|
||||||
|
// side (deferred, since we're inside the already-open popup).
|
||||||
|
PendingPinPick next;
|
||||||
|
next.nodePath = m_pendingPinPick.otherPin.nodePath;
|
||||||
|
next.isOutput = m_pendingPinPick.otherPin.isOutput;
|
||||||
|
next.otherPin = chosen;
|
||||||
|
m_pendingPinPick = next;
|
||||||
|
m_openPinPickMenu = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const PinInfo& outPin = chosen.isOutput ? chosen : m_pendingPinPick.otherPin;
|
||||||
|
const PinInfo& inPin = chosen.isOutput ? m_pendingPinPick.otherPin : chosen;
|
||||||
|
CreateConnection(inPin, outPin);
|
||||||
|
}
|
||||||
|
|
||||||
void MaterialEditorPanel::RenderNodeSearchMenu(const ImVec2& canvasPos, const PinInfo* linkSource) {
|
void MaterialEditorPanel::RenderNodeSearchMenu(const ImVec2& canvasPos, const PinInfo* linkSource) {
|
||||||
if (!m_materialManager || !m_stage || m_materialPath.IsEmpty()) {
|
if (!m_materialManager || !m_stage || m_materialPath.IsEmpty()) {
|
||||||
ImGui::TextDisabled("Open or create a material first");
|
ImGui::TextDisabled("Open or create a material first");
|
||||||
@@ -1937,9 +2373,13 @@ pxr::SdfPath MaterialEditorPanel::CreateShaderNode(const std::string& shaderId,
|
|||||||
void MaterialEditorPanel::CreateConnection(const PinInfo& destInput, const PinInfo& sourceOutput) {
|
void MaterialEditorPanel::CreateConnection(const PinInfo& destInput, const PinInfo& sourceOutput) {
|
||||||
if (!m_stage || !m_commandHistory) return;
|
if (!m_stage || !m_commandHistory) return;
|
||||||
|
|
||||||
|
// Captured now so Undo() (which may run after the ambient edit target
|
||||||
|
// has since changed, e.g. a render-layer switch) still targets the same
|
||||||
|
// layer this connection was authored into.
|
||||||
|
pxr::SdfLayerHandle editLayer = m_stage->GetEditTarget().GetLayer();
|
||||||
m_commandHistory->Push(std::make_unique<ConnectShaderAttrsCommand>(
|
m_commandHistory->Push(std::make_unique<ConnectShaderAttrsCommand>(
|
||||||
m_stage, destInput.nodePath, destInput.name, destInput.typeName,
|
m_stage, destInput.nodePath, destInput.name, destInput.typeName,
|
||||||
sourceOutput.nodePath, sourceOutput.name, sourceOutput.typeName));
|
sourceOutput.nodePath, sourceOutput.name, sourceOutput.typeName, editLayer));
|
||||||
SyncFromUsd();
|
SyncFromUsd();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1951,7 +2391,8 @@ void MaterialEditorPanel::DeleteNode(const pxr::SdfPath& nodePath) {
|
|||||||
|
|
||||||
void MaterialEditorPanel::DisconnectAttr(const pxr::SdfPath& destNode, const std::string& destInput) {
|
void MaterialEditorPanel::DisconnectAttr(const pxr::SdfPath& destNode, const std::string& destInput) {
|
||||||
if (!m_stage || !m_commandHistory) return;
|
if (!m_stage || !m_commandHistory) return;
|
||||||
m_commandHistory->Push(std::make_unique<DisconnectShaderAttrCommand>(m_stage, destNode, destInput));
|
pxr::SdfLayerHandle editLayer = m_stage->GetEditTarget().GetLayer();
|
||||||
|
m_commandHistory->Push(std::make_unique<DisconnectShaderAttrCommand>(m_stage, destNode, destInput, editLayer));
|
||||||
SyncFromUsd();
|
SyncFromUsd();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2038,6 +2479,35 @@ void MaterialEditorPanel::PersistNodePosition(NE::NodeId nodeId) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MaterialEditorPanel::PersistPinDisplayState(const pxr::SdfPath& nodePath) {
|
||||||
|
if (!m_keepGraphNodeViewSettingsInUsd || !m_stage || !m_commandHistory) return;
|
||||||
|
|
||||||
|
pxr::UsdPrim prim = m_stage->GetPrimAtPath(nodePath);
|
||||||
|
if (!prim.IsValid()) return;
|
||||||
|
|
||||||
|
static const pxr::TfToken kPinModeKey("uiPinDisplayMode");
|
||||||
|
|
||||||
|
const int newMode = static_cast<int>(GetPinDisplayOverride(nodePath).mode);
|
||||||
|
|
||||||
|
pxr::VtValue oldModeVt = prim.GetCustomDataByKey(kPinModeKey);
|
||||||
|
const int oldMode = oldModeVt.IsHolding<int>() ? oldModeVt.UncheckedGet<int>() : static_cast<int>(PinDisplayMode::All);
|
||||||
|
|
||||||
|
if (oldMode == newMode)
|
||||||
|
return;
|
||||||
|
|
||||||
|
pxr::UsdStageRefPtr stage = m_stage;
|
||||||
|
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
|
||||||
|
"Change pin display for " + nodePath.GetName(),
|
||||||
|
[stage, nodePath, newMode]() {
|
||||||
|
pxr::UsdPrim p = stage->GetPrimAtPath(nodePath);
|
||||||
|
if (p.IsValid()) p.SetCustomDataByKey(pxr::TfToken("uiPinDisplayMode"), pxr::VtValue(newMode));
|
||||||
|
},
|
||||||
|
[stage, nodePath, oldMode]() {
|
||||||
|
pxr::UsdPrim p = stage->GetPrimAtPath(nodePath);
|
||||||
|
if (p.IsValid()) p.SetCustomDataByKey(pxr::TfToken("uiPinDisplayMode"), pxr::VtValue(oldMode));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
bool MaterialEditorPanel::SaveNodeSettingsCallback(NE::NodeId nodeId, const char* /*data*/, size_t /*size*/,
|
bool MaterialEditorPanel::SaveNodeSettingsCallback(NE::NodeId nodeId, const char* /*data*/, size_t /*size*/,
|
||||||
NE::SaveReasonFlags reason, void* userPointer) {
|
NE::SaveReasonFlags reason, void* userPointer) {
|
||||||
auto* self = static_cast<MaterialEditorPanel*>(userPointer);
|
auto* self = static_cast<MaterialEditorPanel*>(userPointer);
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ public:
|
|||||||
}
|
}
|
||||||
void SetColumnWidths(float browserWidth, float previewWidth);
|
void SetColumnWidths(float browserWidth, float previewWidth);
|
||||||
|
|
||||||
|
/// Whether toggling a node's pin display mode / revealing a hidden pin
|
||||||
|
/// authors USD customData (persisted) or stays session-only. Pushed in by
|
||||||
|
/// Application from AppPreferences, live-updated when the user flips the
|
||||||
|
/// checkbox in Preferences -> Material.
|
||||||
|
void SetKeepGraphNodeViewSettingsInUsd(bool value) { m_keepGraphNodeViewSettingsInUsd = value; }
|
||||||
|
|
||||||
void Render();
|
void Render();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -63,6 +69,12 @@ private:
|
|||||||
std::string name;
|
std::string name;
|
||||||
bool isOutput = false;
|
bool isOutput = false;
|
||||||
pxr::SdfValueTypeName typeName;
|
pxr::SdfValueTypeName typeName;
|
||||||
|
/// True for the collapsed "more pins" connector nub rendered on a
|
||||||
|
/// node's title row when it's hiding unconnected pins (Hypershade
|
||||||
|
/// style) — name/typeName are unset. Connecting to/from it opens
|
||||||
|
/// RenderPinPickMenu() to choose which actual pin to wire, instead of
|
||||||
|
/// creating the connection immediately. See HandleCreateAndDelete().
|
||||||
|
bool isMeta = false;
|
||||||
};
|
};
|
||||||
/// Resolved endpoint of a rendered link, keyed by its ax::NodeEditor LinkId.
|
/// Resolved endpoint of a rendered link, keyed by its ax::NodeEditor LinkId.
|
||||||
struct LinkInfo {
|
struct LinkInfo {
|
||||||
@@ -70,6 +82,24 @@ private:
|
|||||||
std::string destInput;
|
std::string destInput;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Hypershade-style per-node pin display state. Always the render-time
|
||||||
|
/// source of truth (never resync-clobbered); optionally mirrored into USD
|
||||||
|
/// customData when m_keepGraphNodeViewSettingsInUsd is set. See
|
||||||
|
/// GetPinDisplayOverride()/IsPinVisible().
|
||||||
|
struct PinDisplayOverride {
|
||||||
|
PinDisplayMode mode = PinDisplayMode::All;
|
||||||
|
};
|
||||||
|
/// A connection dragged onto/from a collapsed meta pin, awaiting the user
|
||||||
|
/// picking which actual pin on nodePath/isOutput to wire up. otherPin is
|
||||||
|
/// the already-resolved endpoint on the far side of the link (itself meta
|
||||||
|
/// if the user dragged directly between two collapsed nubs, in which case
|
||||||
|
/// picking chains into a second RenderPinPickMenu() for that side).
|
||||||
|
struct PendingPinPick {
|
||||||
|
pxr::SdfPath nodePath;
|
||||||
|
bool isOutput = false;
|
||||||
|
PinInfo otherPin;
|
||||||
|
};
|
||||||
|
|
||||||
void RenderToolbar();
|
void RenderToolbar();
|
||||||
/// Property editor for the node selected in the graph, shown below the
|
/// Property editor for the node selected in the graph, shown below the
|
||||||
/// shader-ball preview. Values apply live while a widget is being
|
/// shader-ball preview. Values apply live while a widget is being
|
||||||
@@ -78,6 +108,14 @@ private:
|
|||||||
void RenderInputValueWidget(const ShaderGraphNode& node, const ShaderPinInfo& input,
|
void RenderInputValueWidget(const ShaderGraphNode& node, const ShaderPinInfo& input,
|
||||||
const pxr::UsdPrim& prim);
|
const pxr::UsdPrim& prim);
|
||||||
void CommitInputEdit(const ShaderGraphNode& node, const ShaderPinInfo& input);
|
void CommitInputEdit(const ShaderGraphNode& node, const ShaderPinInfo& input);
|
||||||
|
/// A "Color Space" row (raw/sRGB/auto/unset) under an Asset-typed input,
|
||||||
|
/// authoring the attribute's colorSpace metadata (UsdAttribute::
|
||||||
|
/// SetColorSpace) — the generic, renderer-agnostic mechanism Hydra reads
|
||||||
|
/// for texture color space (see UsdImagingDataSourceAttributeColorSpace),
|
||||||
|
/// and the only one available for shader types with no separate
|
||||||
|
/// sourceColorSpace input, e.g. MaterialX image nodes.
|
||||||
|
void RenderColorSpaceWidget(const ShaderGraphNode& node, const ShaderPinInfo& input,
|
||||||
|
const pxr::UsdPrim& prim);
|
||||||
/// Hypershade-style browser: lists every material on the stage; the
|
/// Hypershade-style browser: lists every material on the stage; the
|
||||||
/// selected one is loaded into the graph work area via "Show Graph"
|
/// selected one is loaded into the graph work area via "Show Graph"
|
||||||
/// (or double-click).
|
/// (or double-click).
|
||||||
@@ -122,6 +160,45 @@ private:
|
|||||||
void PersistNodePosition(ax::NodeEditor::NodeId nodeId);
|
void PersistNodePosition(ax::NodeEditor::NodeId nodeId);
|
||||||
bool IsPinLinked(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const;
|
bool IsPinLinked(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const;
|
||||||
|
|
||||||
|
/// Seeds m_pinDisplayOverrides for any node path not already present
|
||||||
|
/// (i.e. not touched yet this session) from the just-synced m_graph's
|
||||||
|
/// pinDisplayMode (customData-backed). Never overwrites an existing
|
||||||
|
/// entry, so in-session toggles survive resyncs.
|
||||||
|
void SeedPinDisplayOverrides();
|
||||||
|
/// Creates the entry (defaulted to All) on first access.
|
||||||
|
PinDisplayOverride& GetPinDisplayOverride(const pxr::SdfPath& nodePath);
|
||||||
|
/// True if pin should be drawn this frame: All mode shows everything;
|
||||||
|
/// Connected mode shows only linked pins (linked pins are never hidden —
|
||||||
|
/// Link() unconditionally references pin IDs that must have been drawn —
|
||||||
|
/// so a hidden pin becomes visible the moment it's connected, via
|
||||||
|
/// RenderPinPickMenu()); Collapsed mode hides every real pin, linked or
|
||||||
|
/// not, in favor of the single connector nub per side (see
|
||||||
|
/// EffectivePinId(), which redirects existing links to that nub).
|
||||||
|
bool IsPinVisible(const ShaderGraphNode& node, const std::string& pinName, bool isOutput) const;
|
||||||
|
/// Cycles a node's display mode: All -> Connected -> Collapsed -> All.
|
||||||
|
void CyclePinDisplayMode(const pxr::SdfPath& nodePath);
|
||||||
|
/// Sets every node currently in m_graph to mode (toolbar "set all" button).
|
||||||
|
void ApplyPinDisplayModeToAllNodes(PinDisplayMode mode);
|
||||||
|
/// Authors the node's current PinDisplayOverride into USD customData via
|
||||||
|
/// an undoable command, mirroring PersistNodePosition. No-op unless
|
||||||
|
/// m_keepGraphNodeViewSettingsInUsd is set.
|
||||||
|
void PersistPinDisplayState(const pxr::SdfPath& nodePath);
|
||||||
|
/// PinId to use when drawing a link touching (nodePath, pinName,
|
||||||
|
/// isOutput): the pin's own id normally, or that node's collapsed meta
|
||||||
|
/// nub id when the node is in Collapsed mode (so existing links visually
|
||||||
|
/// converge on the nub instead of referencing an undrawn real pin).
|
||||||
|
uintptr_t EffectivePinId(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const;
|
||||||
|
/// Renders the deferred "choose pin" popup for m_pendingPinPick (Maya
|
||||||
|
/// Hypershade style): a connection was dragged onto/from a collapsed meta
|
||||||
|
/// pin, and the user now picks which actual attribute it wires to. Must
|
||||||
|
/// be called from within the NE::Suspend()/Resume() block, like the
|
||||||
|
/// other popups.
|
||||||
|
void RenderPinPickMenu();
|
||||||
|
/// Finishes a pick: creates the real connection to/from chosen, unless
|
||||||
|
/// the far side of the pending pick was itself a meta pin, in which case
|
||||||
|
/// this chains into a second pick for that side instead.
|
||||||
|
void CompletePinPick(const PinInfo& chosen);
|
||||||
|
|
||||||
static bool SaveNodeSettingsCallback(ax::NodeEditor::NodeId nodeId,
|
static bool SaveNodeSettingsCallback(ax::NodeEditor::NodeId nodeId,
|
||||||
const char* data, size_t size,
|
const char* data, size_t size,
|
||||||
ax::NodeEditor::SaveReasonFlags reason,
|
ax::NodeEditor::SaveReasonFlags reason,
|
||||||
@@ -133,6 +210,15 @@ private:
|
|||||||
CommandHistory* m_commandHistory = nullptr;
|
CommandHistory* m_commandHistory = nullptr;
|
||||||
IconManager* m_iconManager = nullptr;
|
IconManager* m_iconManager = nullptr;
|
||||||
|
|
||||||
|
/// Last color correction pushed via SetColorCorrectionFromPrefs, cached
|
||||||
|
/// so RenderNodeGraphCanvas can pass it to NodeThumbnailCache every frame
|
||||||
|
/// (keeps in-canvas node thumbnails matching m_preview/the app global).
|
||||||
|
int m_ccMode = 1; // ColorCorrectionMode cast to int (1 = sRGB), mirrors AppPreferences' default
|
||||||
|
std::string m_ocioDisplay;
|
||||||
|
std::string m_ocioView;
|
||||||
|
std::string m_ocioColorSpace;
|
||||||
|
std::string m_ocioLook;
|
||||||
|
|
||||||
/// Material currently shown in the graph work area (empty = none).
|
/// Material currently shown in the graph work area (empty = none).
|
||||||
pxr::SdfPath m_materialPath;
|
pxr::SdfPath m_materialPath;
|
||||||
/// Material highlighted in the browser list; becomes m_materialPath when
|
/// Material highlighted in the browser list; becomes m_materialPath when
|
||||||
@@ -170,6 +256,27 @@ private:
|
|||||||
bool m_openLinkDragMenu = false;
|
bool m_openLinkDragMenu = false;
|
||||||
PinInfo m_linkDragSourcePin;
|
PinInfo m_linkDragSourcePin;
|
||||||
|
|
||||||
|
/// Hypershade-style pin display state, keyed by node path string. Always
|
||||||
|
/// the render-time source of truth; see PinDisplayOverride.
|
||||||
|
std::unordered_map<std::string, PinDisplayOverride> m_pinDisplayOverrides;
|
||||||
|
/// Whether node toggles author USD customData (see
|
||||||
|
/// SetKeepGraphNodeViewSettingsInUsd).
|
||||||
|
bool m_keepGraphNodeViewSettingsInUsd = false;
|
||||||
|
/// Deferred "choose pin" popup request: set from HandleCreateAndDelete()
|
||||||
|
/// when a link is dropped on/dragged from a collapsed meta pin, actually
|
||||||
|
/// opened after NE::Suspend(), like m_openLinkDragMenu.
|
||||||
|
bool m_openPinPickMenu = false;
|
||||||
|
PendingPinPick m_pendingPinPick;
|
||||||
|
|
||||||
|
/// Toolbar toggle: whether node thumbnails (texture images / material
|
||||||
|
/// shader-ball renders) are shown at all. Off skips the reserved space
|
||||||
|
/// and the decode/render work entirely, not just the visual.
|
||||||
|
bool m_showNodeThumbnails = true;
|
||||||
|
/// Toolbar "set all nodes" pin-display button: the mode last broadcast
|
||||||
|
/// to every node (and the icon/tooltip shown), independent of what any
|
||||||
|
/// individual node is actually in after the user tweaks one by hand.
|
||||||
|
PinDisplayMode m_toolbarPinDisplayMode = PinDisplayMode::All;
|
||||||
|
|
||||||
/// Column widths of the browser (left) and preview (right) sections,
|
/// Column widths of the browser (left) and preview (right) sections,
|
||||||
/// user-adjustable via the splitters between them and the canvas.
|
/// user-adjustable via the splitters between them and the canvas.
|
||||||
float m_browserWidth = 220.0f;
|
float m_browserWidth = 220.0f;
|
||||||
|
|||||||
@@ -592,6 +592,17 @@ void MaterialPreviewRenderer::SetColorCorrection(int ccMode, const std::string&
|
|||||||
m_dirty = true;
|
m_dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MaterialPreviewRenderer::SetRendererPlugin(const pxr::TfToken& pluginId) {
|
||||||
|
if (pluginId == m_renderer.GetCurrentRendererId()) return;
|
||||||
|
m_renderer.SetRendererPlugin(pluginId);
|
||||||
|
UpdateDomeOrientation(); // pole convention differs per delegate
|
||||||
|
m_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pxr::TfToken MaterialPreviewRenderer::GetRendererPlugin() const {
|
||||||
|
return m_renderer.GetCurrentRendererId();
|
||||||
|
}
|
||||||
|
|
||||||
void MaterialPreviewRenderer::RenderRendererDropdown(float width) {
|
void MaterialPreviewRenderer::RenderRendererDropdown(float width) {
|
||||||
pxr::TfToken currentId = m_renderer.GetCurrentRendererId();
|
pxr::TfToken currentId = m_renderer.GetCurrentRendererId();
|
||||||
std::string displayName = currentId.IsEmpty()
|
std::string displayName = currentId.IsEmpty()
|
||||||
@@ -607,11 +618,8 @@ void MaterialPreviewRenderer::RenderRendererDropdown(float width) {
|
|||||||
std::string name = UsdSceneRenderer::GetRendererDisplayName(pluginId);
|
std::string name = UsdSceneRenderer::GetRendererDisplayName(pluginId);
|
||||||
if (name.empty()) name = pluginId.GetString();
|
if (name.empty()) name = pluginId.GetString();
|
||||||
bool selected = (pluginId == currentId);
|
bool selected = (pluginId == currentId);
|
||||||
if (ImGui::MenuItem(name.c_str(), nullptr, selected) && !selected) {
|
if (ImGui::MenuItem(name.c_str(), nullptr, selected) && !selected)
|
||||||
m_renderer.SetRendererPlugin(pluginId);
|
SetRendererPlugin(pluginId);
|
||||||
UpdateDomeOrientation(); // pole convention differs per delegate
|
|
||||||
m_dirty = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ImGui::EndPopup();
|
ImGui::EndPopup();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,22 @@ public:
|
|||||||
const std::string& ocioColorSpace,
|
const std::string& ocioColorSpace,
|
||||||
const std::string& ocioLook);
|
const std::string& ocioLook);
|
||||||
|
|
||||||
|
/// Switches the shader-ball's render delegate (what RenderRendererDropdown
|
||||||
|
/// does on click). No-op if already this plugin. Exposed so other
|
||||||
|
/// consumers of a material preview (e.g. NodeThumbnailCache's in-canvas
|
||||||
|
/// thumbnails) can mirror this renderer's current choice.
|
||||||
|
void SetRendererPlugin(const pxr::TfToken& pluginId);
|
||||||
|
/// Current render delegate (empty until EnsureInitialized() has run).
|
||||||
|
pxr::TfToken GetRendererPlugin() const;
|
||||||
|
|
||||||
|
/// True once the last Render() call's image is final. Progressive
|
||||||
|
/// delegates (Arnold/Cycles/Embree) return false for many calls after a
|
||||||
|
/// material/renderer change while they accumulate samples — callers that
|
||||||
|
/// cache the rendered texture (e.g. NodeThumbnailCache) must keep
|
||||||
|
/// re-rendering until this is true, or they'll cache an early noisy (or
|
||||||
|
/// still-black) frame.
|
||||||
|
bool IsConverged() const { return m_renderer.IsConverged(); }
|
||||||
|
|
||||||
/// Renderer-delegate picker UI (matches the viewport's dropdown pattern).
|
/// Renderer-delegate picker UI (matches the viewport's dropdown pattern).
|
||||||
/// width: ImGui item width (-1 = fill available).
|
/// width: ImGui item width (-1 = fill available).
|
||||||
void RenderRendererDropdown(float width = -1.0f);
|
void RenderRendererDropdown(float width = -1.0f);
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ bool ImGuiEx::Canvas::Begin(ImGuiID id, const ImVec2& size)
|
|||||||
# endif
|
# endif
|
||||||
|
|
||||||
SaveInputState();
|
SaveInputState();
|
||||||
|
SaveViewportState();
|
||||||
|
|
||||||
EnterLocalSpace();
|
EnterLocalSpace();
|
||||||
|
|
||||||
@@ -319,6 +320,46 @@ void ImGuiEx::Canvas::RestoreInputState()
|
|||||||
ImGui::GetCurrentWindow()->DC.CursorMaxPos = m_WindowCursorMaxBackup;
|
ImGui::GetCurrentWindow()->DC.CursorMaxPos = m_WindowCursorMaxBackup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UsdLayerManager local patch, ported from upstream imgui-node-editor
|
||||||
|
// (see the declaration in imgui_canvas.h for why).
|
||||||
|
void ImGuiEx::Canvas::SaveViewportState()
|
||||||
|
{
|
||||||
|
# if defined(IMGUI_HAS_VIEWPORT)
|
||||||
|
auto window = ImGui::GetCurrentWindow();
|
||||||
|
auto viewport = ImGui::GetWindowViewport();
|
||||||
|
|
||||||
|
m_WindowPosBackup = window->Pos;
|
||||||
|
m_ViewportPosBackup = viewport->Pos;
|
||||||
|
m_ViewportSizeBackup = viewport->Size;
|
||||||
|
# if IMGUI_VERSION_NUM > 18002
|
||||||
|
m_ViewportWorkPosBackup = viewport->WorkPos;
|
||||||
|
m_ViewportWorkSizeBackup = viewport->WorkSize;
|
||||||
|
# else
|
||||||
|
m_ViewportWorkOffsetMinBackup = viewport->WorkOffsetMin;
|
||||||
|
m_ViewportWorkOffsetMaxBackup = viewport->WorkOffsetMax;
|
||||||
|
# endif
|
||||||
|
# endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGuiEx::Canvas::RestoreViewportState()
|
||||||
|
{
|
||||||
|
# if defined(IMGUI_HAS_VIEWPORT)
|
||||||
|
auto window = ImGui::GetCurrentWindow();
|
||||||
|
auto viewport = ImGui::GetWindowViewport();
|
||||||
|
|
||||||
|
window->Pos = m_WindowPosBackup;
|
||||||
|
viewport->Pos = m_ViewportPosBackup;
|
||||||
|
viewport->Size = m_ViewportSizeBackup;
|
||||||
|
# if IMGUI_VERSION_NUM > 18002
|
||||||
|
viewport->WorkPos = m_ViewportWorkPosBackup;
|
||||||
|
viewport->WorkSize = m_ViewportWorkSizeBackup;
|
||||||
|
# else
|
||||||
|
viewport->WorkOffsetMin = m_ViewportWorkOffsetMinBackup;
|
||||||
|
viewport->WorkOffsetMax = m_ViewportWorkOffsetMaxBackup;
|
||||||
|
# endif
|
||||||
|
# endif
|
||||||
|
}
|
||||||
|
|
||||||
void ImGuiEx::Canvas::EnterLocalSpace()
|
void ImGuiEx::Canvas::EnterLocalSpace()
|
||||||
{
|
{
|
||||||
// Prepare ImDrawList for drawing in local coordinate system:
|
// Prepare ImDrawList for drawing in local coordinate system:
|
||||||
@@ -374,6 +415,38 @@ void ImGuiEx::Canvas::EnterLocalSpace()
|
|||||||
for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i)
|
for (auto i = 0; i < IM_ARRAYSIZE(m_MouseClickedPosBackup); ++i)
|
||||||
io.MouseClickedPos[i] = (m_MouseClickedPosBackup[i] - m_ViewTransformPosition) * m_View.InvScale;
|
io.MouseClickedPos[i] = (m_MouseClickedPosBackup[i] - m_ViewTransformPosition) * m_View.InvScale;
|
||||||
|
|
||||||
|
// UsdLayerManager local patch, ported from upstream imgui-node-editor:
|
||||||
|
// move the viewport rect into local space too, so IsMouseHoveringRect's
|
||||||
|
// screen-space viewport overlap check (docking-branch ImGui) passes for
|
||||||
|
// items at canvas coordinates outside the screen range (e.g. negative).
|
||||||
|
// See SaveViewportState() declaration for the full story.
|
||||||
|
# if defined(IMGUI_HAS_VIEWPORT)
|
||||||
|
{
|
||||||
|
auto window = ImGui::GetCurrentWindow();
|
||||||
|
window->Pos = ImVec2(0.0f, 0.0f);
|
||||||
|
|
||||||
|
auto viewport_min = m_ViewportPosBackup;
|
||||||
|
auto viewport_max = m_ViewportPosBackup + m_ViewportSizeBackup;
|
||||||
|
|
||||||
|
viewport_min.x = (viewport_min.x - m_ViewTransformPosition.x) * m_View.InvScale;
|
||||||
|
viewport_min.y = (viewport_min.y - m_ViewTransformPosition.y) * m_View.InvScale;
|
||||||
|
viewport_max.x = (viewport_max.x - m_ViewTransformPosition.x) * m_View.InvScale;
|
||||||
|
viewport_max.y = (viewport_max.y - m_ViewTransformPosition.y) * m_View.InvScale;
|
||||||
|
|
||||||
|
auto viewport = ImGui::GetWindowViewport();
|
||||||
|
viewport->Pos = viewport_min;
|
||||||
|
viewport->Size = viewport_max - viewport_min;
|
||||||
|
|
||||||
|
# if IMGUI_VERSION_NUM > 18002
|
||||||
|
viewport->WorkPos = m_ViewportWorkPosBackup * m_View.InvScale;
|
||||||
|
viewport->WorkSize = m_ViewportWorkSizeBackup * m_View.InvScale;
|
||||||
|
# else
|
||||||
|
viewport->WorkOffsetMin = m_ViewportWorkOffsetMinBackup * m_View.InvScale;
|
||||||
|
viewport->WorkOffsetMax = m_ViewportWorkOffsetMaxBackup * m_View.InvScale;
|
||||||
|
# endif
|
||||||
|
}
|
||||||
|
# endif
|
||||||
|
|
||||||
m_ViewRect = CalcViewRect(m_View);;
|
m_ViewRect = CalcViewRect(m_View);;
|
||||||
|
|
||||||
auto& fringeScale = ImFringeScaleRef(m_DrawList);
|
auto& fringeScale = ImFringeScaleRef(m_DrawList);
|
||||||
@@ -449,4 +522,5 @@ void ImGuiEx::Canvas::LeaveLocalSpace()
|
|||||||
ImGui::PopClipRect();
|
ImGui::PopClipRect();
|
||||||
|
|
||||||
RestoreInputState();
|
RestoreInputState();
|
||||||
|
RestoreViewportState();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,6 +210,18 @@ private:
|
|||||||
void SaveInputState();
|
void SaveInputState();
|
||||||
void RestoreInputState();
|
void RestoreInputState();
|
||||||
|
|
||||||
|
// UsdLayerManager local patch, ported from upstream imgui-node-editor:
|
||||||
|
// with a viewport-enabled ImGui (IMGUI_HAS_VIEWPORT, i.e. the docking
|
||||||
|
// branch), ImGui::IsMouseHoveringRect() — and therefore every item's
|
||||||
|
// hover test — requires the rect to overlap the mouse viewport's rect,
|
||||||
|
// which is in *screen* space. Inside the canvas, rects are in canvas
|
||||||
|
// space, so anything at canvas coordinates outside the screen-space
|
||||||
|
// range (e.g. negative) went hover/click-dead. Fix: transform the
|
||||||
|
// viewport rect into canvas-local space while inside EnterLocalSpace()
|
||||||
|
// and restore it on LeaveLocalSpace(), exactly like the mouse position.
|
||||||
|
void SaveViewportState();
|
||||||
|
void RestoreViewportState();
|
||||||
|
|
||||||
void EnterLocalSpace();
|
void EnterLocalSpace();
|
||||||
void LeaveLocalSpace();
|
void LeaveLocalSpace();
|
||||||
|
|
||||||
@@ -243,6 +255,19 @@ private:
|
|||||||
ImVec2 m_MousePosPrevBackup;
|
ImVec2 m_MousePosPrevBackup;
|
||||||
ImVec2 m_MouseClickedPosBackup[IM_ARRAYSIZE(ImGuiIO::MouseClickedPos)];
|
ImVec2 m_MouseClickedPosBackup[IM_ARRAYSIZE(ImGuiIO::MouseClickedPos)];
|
||||||
ImVec2 m_WindowCursorMaxBackup;
|
ImVec2 m_WindowCursorMaxBackup;
|
||||||
|
|
||||||
|
# if defined(IMGUI_HAS_VIEWPORT)
|
||||||
|
ImVec2 m_WindowPosBackup;
|
||||||
|
ImVec2 m_ViewportPosBackup;
|
||||||
|
ImVec2 m_ViewportSizeBackup;
|
||||||
|
# if IMGUI_VERSION_NUM > 18002
|
||||||
|
ImVec2 m_ViewportWorkPosBackup;
|
||||||
|
ImVec2 m_ViewportWorkSizeBackup;
|
||||||
|
# else
|
||||||
|
ImVec2 m_ViewportWorkOffsetMinBackup;
|
||||||
|
ImVec2 m_ViewportWorkOffsetMaxBackup;
|
||||||
|
# endif
|
||||||
|
# endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ImGuiEx
|
} // namespace ImGuiEx
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#include "NodeThumbnailCache.h"
|
#include "NodeThumbnailCache.h"
|
||||||
#include "../utils/GLExt.h"
|
#include "../utils/GLExt.h"
|
||||||
#include "../utils/Logger.h"
|
|
||||||
|
|
||||||
#include <pxr/imaging/hio/image.h>
|
#include <pxr/imaging/hio/image.h>
|
||||||
#include <pxr/imaging/hio/types.h>
|
#include <pxr/imaging/hio/types.h>
|
||||||
@@ -243,6 +242,32 @@ ImTextureID NodeThumbnailCache::GetMaterialThumbnail(const pxr::UsdStageRefPtr&
|
|||||||
return e.texId;
|
return e.texId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NodeThumbnailCache::SyncRenderSettings(const pxr::TfToken& rendererPlugin, int ccMode,
|
||||||
|
const std::string& ocioDisplay, const std::string& ocioView,
|
||||||
|
const std::string& ocioColorSpace, const std::string& ocioLook) {
|
||||||
|
const bool changed = !m_renderSettingsInitialized ||
|
||||||
|
rendererPlugin != m_lastRendererPlugin || ccMode != m_lastCcMode ||
|
||||||
|
ocioDisplay != m_lastOcioDisplay || ocioView != m_lastOcioView ||
|
||||||
|
ocioColorSpace != m_lastOcioColorSpace || ocioLook != m_lastOcioLook;
|
||||||
|
if (!changed) return;
|
||||||
|
|
||||||
|
m_renderSettingsInitialized = true;
|
||||||
|
m_lastRendererPlugin = rendererPlugin;
|
||||||
|
m_lastCcMode = ccMode;
|
||||||
|
m_lastOcioDisplay = ocioDisplay;
|
||||||
|
m_lastOcioView = ocioView;
|
||||||
|
m_lastOcioColorSpace = ocioColorSpace;
|
||||||
|
m_lastOcioLook = ocioLook;
|
||||||
|
|
||||||
|
if (!rendererPlugin.IsEmpty())
|
||||||
|
m_matRenderer.SetRendererPlugin(rendererPlugin);
|
||||||
|
m_matRenderer.SetColorCorrection(ccMode, ocioDisplay, ocioView, ocioColorSpace, ocioLook);
|
||||||
|
|
||||||
|
// Every cached shader-ball thumbnail was rendered under the old settings.
|
||||||
|
for (auto& kv : m_matEntries)
|
||||||
|
kv.second.stale = true;
|
||||||
|
}
|
||||||
|
|
||||||
void NodeThumbnailCache::EnsureBlitFbos() {
|
void NodeThumbnailCache::EnsureBlitFbos() {
|
||||||
if (!m_readFbo) glGenFramebuffers(1, &m_readFbo);
|
if (!m_readFbo) glGenFramebuffers(1, &m_readFbo);
|
||||||
if (!m_drawFbo) glGenFramebuffers(1, &m_drawFbo);
|
if (!m_drawFbo) glGenFramebuffers(1, &m_drawFbo);
|
||||||
@@ -317,7 +342,11 @@ void NodeThumbnailCache::PumpMainThread() {
|
|||||||
const uint32_t src = m_matRenderer.Render(kThumbPx, kThumbPx);
|
const uint32_t src = m_matRenderer.Render(kThumbPx, kThumbPx);
|
||||||
if (src != 0) {
|
if (src != 0) {
|
||||||
BlitToEntry(e, src);
|
BlitToEntry(e, src);
|
||||||
e.stale = false;
|
// Progressive delegates (Arnold/Cycles/Embree) need several
|
||||||
|
// Render() calls to accumulate past their first noisy sample;
|
||||||
|
// keep re-pumping this entry until the image is final instead of
|
||||||
|
// caching whatever the first pass produced.
|
||||||
|
e.stale = !m_matRenderer.IsConverged();
|
||||||
}
|
}
|
||||||
break; // one per frame
|
break; // one per frame
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "MaterialPreviewRenderer.h"
|
#include "MaterialPreviewRenderer.h"
|
||||||
#include <pxr/usd/usd/stage.h>
|
#include <pxr/usd/usd/stage.h>
|
||||||
#include <pxr/usd/sdf/path.h>
|
#include <pxr/usd/sdf/path.h>
|
||||||
|
#include <pxr/base/tf/token.h>
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
@@ -53,6 +54,18 @@ public:
|
|||||||
const std::string& output, bool terminal,
|
const std::string& output, bool terminal,
|
||||||
size_t revision);
|
size_t revision);
|
||||||
|
|
||||||
|
/// Keeps the material shader-ball thumbnails' render delegate and color
|
||||||
|
/// correction in sync with the main preview swatch's current settings
|
||||||
|
/// (they'd otherwise render under this cache's own independent Hydra
|
||||||
|
/// instance's defaults, mismatching the preview and the app's global
|
||||||
|
/// color-correction). Cheap to call every frame: no-ops unless something
|
||||||
|
/// actually changed, and only then invalidates cached thumbnails so
|
||||||
|
/// PumpMainThread re-renders them under the new settings. An empty
|
||||||
|
/// rendererPlugin means "not resolved yet" and is left alone.
|
||||||
|
void SyncRenderSettings(const pxr::TfToken& rendererPlugin, int ccMode,
|
||||||
|
const std::string& ocioDisplay, const std::string& ocioView,
|
||||||
|
const std::string& ocioColorSpace, const std::string& ocioLook);
|
||||||
|
|
||||||
/// Main thread, once per frame, GL context current: drains finished
|
/// Main thread, once per frame, GL context current: drains finished
|
||||||
/// decodes and uploads them, renders at most one stale material thumbnail,
|
/// decodes and uploads them, renders at most one stale material thumbnail,
|
||||||
/// and prunes entries not requested since the previous pump.
|
/// and prunes entries not requested since the previous pump.
|
||||||
@@ -99,6 +112,13 @@ private:
|
|||||||
unsigned int m_readFbo = 0; // scratch GL FBOs for the render->cache blit
|
unsigned int m_readFbo = 0; // scratch GL FBOs for the render->cache blit
|
||||||
unsigned int m_drawFbo = 0;
|
unsigned int m_drawFbo = 0;
|
||||||
|
|
||||||
|
// Last render settings applied via SyncRenderSettings, so repeated
|
||||||
|
// per-frame calls with unchanged values are cheap no-ops.
|
||||||
|
bool m_renderSettingsInitialized = false;
|
||||||
|
pxr::TfToken m_lastRendererPlugin;
|
||||||
|
int m_lastCcMode = -1;
|
||||||
|
std::string m_lastOcioDisplay, m_lastOcioView, m_lastOcioColorSpace, m_lastOcioLook;
|
||||||
|
|
||||||
void EnsureBlitFbos();
|
void EnsureBlitFbos();
|
||||||
void BlitToEntry(MatEntry& e, uint32_t srcTex);
|
void BlitToEntry(MatEntry& e, uint32_t srcTex);
|
||||||
static void DeleteTex(ImTextureID& id);
|
static void DeleteTex(ImTextureID& id);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "../core/commands/RenamePrimCommand.h"
|
#include "../core/commands/RenamePrimCommand.h"
|
||||||
#include "../core/commands/ReparentPrimCommand.h"
|
#include "../core/commands/ReparentPrimCommand.h"
|
||||||
#include "../core/commands/GroupPrimsCommand.h"
|
#include "../core/commands/GroupPrimsCommand.h"
|
||||||
|
#include "../core/commands/LayerCommands.h"
|
||||||
#include <pxr/usd/usd/prim.h>
|
#include <pxr/usd/usd/prim.h>
|
||||||
#include <pxr/usd/usd/primRange.h>
|
#include <pxr/usd/usd/primRange.h>
|
||||||
#include <pxr/usd/usd/references.h>
|
#include <pxr/usd/usd/references.h>
|
||||||
@@ -309,6 +310,8 @@ void SceneHierarchyPanel::Render() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
|
||||||
|
ImGui::BeginDisabled(renderLayerActive);
|
||||||
ImGui::SetNextItemWidth(-1.0f);
|
ImGui::SetNextItemWidth(-1.0f);
|
||||||
if (ImGui::BeginCombo("##layerswitch", currentLabel.c_str(),
|
if (ImGui::BeginCombo("##layerswitch", currentLabel.c_str(),
|
||||||
ImGuiComboFlags_HeightRegular)) {
|
ImGuiComboFlags_HeightRegular)) {
|
||||||
@@ -326,9 +329,16 @@ void SceneHierarchyPanel::Render() {
|
|||||||
}
|
}
|
||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
}
|
}
|
||||||
|
bool comboHovered = ImGui::IsItemHovered();
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
if (renderLayerActive && comboHovered)
|
||||||
|
ImGui::SetTooltip("Edit target is locked to the active render layer");
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RenderSublayerSection();
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
auto paths = m_propertyManager->GetPrimPaths();
|
auto paths = m_propertyManager->GetPrimPaths();
|
||||||
if (paths.empty()) {
|
if (paths.empty()) {
|
||||||
ImGui::TextDisabled("No prims in stage");
|
ImGui::TextDisabled("No prims in stage");
|
||||||
@@ -582,6 +592,272 @@ void SceneHierarchyPanel::Render() {
|
|||||||
ProcessPendingReplaceRef();
|
ProcessPendingReplaceRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SceneHierarchyPanel::RenderSublayerSection() {
|
||||||
|
if (!ImGui::CollapsingHeader("Sublayers", ImGuiTreeNodeFlags_DefaultOpen))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!m_layerManager) {
|
||||||
|
ImGui::TextDisabled(" No layer manager");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto sublayers = m_layerManager->GetSublayers();
|
||||||
|
if (sublayers.empty()) {
|
||||||
|
ImGui::TextDisabled(" No sublayers");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float rowHeight = ImGui::GetTextLineHeightWithSpacing();
|
||||||
|
const int visibleRows = std::min<int>(static_cast<int>(sublayers.size()), 6);
|
||||||
|
float tableHeight = rowHeight * (visibleRows + 1); // +1 for header row
|
||||||
|
|
||||||
|
if (!ImGui::BeginTable("HierarchySublayerTable", 6,
|
||||||
|
ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
|
||||||
|
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY,
|
||||||
|
ImVec2(0, tableHeight)))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ImGui::TableSetupScrollFreeze(0, 1);
|
||||||
|
ImGui::TableSetupColumn("##drag", ImGuiTableColumnFlags_WidthFixed, 14.0f);
|
||||||
|
ImGui::TableSetupColumn("##dirty", ImGuiTableColumnFlags_WidthFixed, 14.0f);
|
||||||
|
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
|
||||||
|
ImGui::TableSetupColumn("##et", ImGuiTableColumnFlags_WidthFixed, 28.0f);
|
||||||
|
ImGui::TableSetupColumn("Muted", ImGuiTableColumnFlags_WidthFixed, 55.0f);
|
||||||
|
ImGui::TableSetupColumn("##order", ImGuiTableColumnFlags_WidthFixed, 44.0f);
|
||||||
|
|
||||||
|
// Custom header row: pen icon for the ET column, text for the rest.
|
||||||
|
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
|
||||||
|
ImGui::TableSetColumnIndex(2); ImGui::TableHeader("Name");
|
||||||
|
ImGui::TableSetColumnIndex(3);
|
||||||
|
{
|
||||||
|
float cellW = ImGui::GetContentRegionAvail().x;
|
||||||
|
float iconW = 14.0f;
|
||||||
|
float off = (cellW - iconW) * 0.5f;
|
||||||
|
if (off > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + off);
|
||||||
|
if (m_iconManager)
|
||||||
|
ImGui::Image(ImTextureRef(m_iconManager->Get(Icon::Pen)), ImVec2(iconW, iconW));
|
||||||
|
else
|
||||||
|
ImGui::TextUnformatted("ET");
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Edit Target");
|
||||||
|
}
|
||||||
|
ImGui::TableSetColumnIndex(4); ImGui::TableHeader("Muted");
|
||||||
|
ImGui::TableSetColumnIndex(5); ImGui::TableHeader("Order");
|
||||||
|
|
||||||
|
for (int i = 0; i < static_cast<int>(sublayers.size()); i++) {
|
||||||
|
const auto& li = sublayers[i];
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::PushID(i);
|
||||||
|
|
||||||
|
// Col 0: invisible span-all Selectable for row selection / drag / context menu.
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImVec2 rowStart = ImGui::GetCursorScreenPos(); // save before Selectable moves cursor
|
||||||
|
|
||||||
|
bool isSelected = (m_sublayerSelectedIdx == i);
|
||||||
|
if (ImGui::Selectable("##row", isSelected,
|
||||||
|
ImGuiSelectableFlags_SpanAllColumns |
|
||||||
|
ImGuiSelectableFlags_AllowOverlap,
|
||||||
|
ImVec2(0, 0)))
|
||||||
|
m_sublayerSelectedIdx = i;
|
||||||
|
|
||||||
|
// Context menu: must come immediately after the span-all Selectable so
|
||||||
|
// BeginPopupContextItem sees it as the "last item" and responds to
|
||||||
|
// right-click anywhere in the row.
|
||||||
|
RenderSublayerContextMenu(i, sublayers);
|
||||||
|
|
||||||
|
// Drag source: also attached to the Selectable (left-button drag).
|
||||||
|
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
|
||||||
|
ImGui::SetDragDropPayload("HIER_SUBLAYER_IDX", &i, sizeof(int));
|
||||||
|
ImGui::Text("%s", li.displayName.c_str());
|
||||||
|
ImGui::EndDragDropSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop target on the Selectable.
|
||||||
|
if (ImGui::BeginDragDropTarget()) {
|
||||||
|
if (const ImGuiPayload* payload =
|
||||||
|
ImGui::AcceptDragDropPayload("HIER_SUBLAYER_IDX")) {
|
||||||
|
int src = *static_cast<const int*>(payload->Data);
|
||||||
|
int dst = i;
|
||||||
|
if (src != dst) {
|
||||||
|
std::vector<std::string> before, after;
|
||||||
|
for (const auto& sl : sublayers) before.push_back(sl.identifier);
|
||||||
|
after = before;
|
||||||
|
std::string moved = after[src];
|
||||||
|
after.erase(after.begin() + src);
|
||||||
|
after.insert(after.begin() + dst, moved);
|
||||||
|
if (m_commandHistory) {
|
||||||
|
m_commandHistory->Push(std::make_unique<LayerReorderCommand>(
|
||||||
|
m_layerManager, before, after));
|
||||||
|
} else {
|
||||||
|
LayerReorderCommand cmd(m_layerManager, before, after);
|
||||||
|
static_cast<ICommand&>(cmd).Execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndDragDropTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// "=" drag handle: overlay at col 0 using the saved screen position so it
|
||||||
|
// sits inside the row, not below it (TableSetColumnIndex only resets X).
|
||||||
|
ImGui::SetCursorScreenPos(rowStart);
|
||||||
|
ImGui::TextDisabled("=");
|
||||||
|
|
||||||
|
// Col 1: dirty indicator
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
if (li.isDirty)
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "*");
|
||||||
|
else
|
||||||
|
ImGui::TextDisabled(" ");
|
||||||
|
|
||||||
|
bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
|
||||||
|
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
|
||||||
|
|
||||||
|
// Col 2: layer name
|
||||||
|
ImGui::TableSetColumnIndex(2);
|
||||||
|
ImVec4 nameColor = li.isMuted
|
||||||
|
? ImVec4(0.5f, 0.5f, 0.5f, 1.0f)
|
||||||
|
: ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||||
|
ImGui::TextColored(nameColor, "%s", li.displayName.c_str());
|
||||||
|
if (ImGui::IsItemHovered() && !li.realPath.empty())
|
||||||
|
ImGui::SetTooltip("%s\n%s", li.identifier.c_str(), li.realPath.c_str());
|
||||||
|
if (isRenderLayer) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextColored(ImVec4(0.9f, 0.7f, 0.2f, 1.0f), "[RL]");
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("Managed from the Render Layer panel");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Col 3: edit target checkbox — checking sets this layer as edit target;
|
||||||
|
// unchecking does nothing (there is always an active edit target).
|
||||||
|
ImGui::TableSetColumnIndex(3);
|
||||||
|
{
|
||||||
|
bool isET = li.isEditTarget;
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
|
||||||
|
ImGui::BeginDisabled(renderLayerActive);
|
||||||
|
if (ImGui::Checkbox("##et", &isET) && isET)
|
||||||
|
m_layerManager->SetEditTarget(li.identifier);
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip(renderLayerActive ? "Edit target is locked to the active render layer"
|
||||||
|
: li.isEditTarget ? "Edit target (active)"
|
||||||
|
: "Set as edit target");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Col 4: mute checkbox — disabled for render-layer sublayers, which
|
||||||
|
// RenderLayerManager mutes/unmutes exclusively as layers are switched.
|
||||||
|
ImGui::TableSetColumnIndex(4);
|
||||||
|
{
|
||||||
|
bool muted = li.isMuted;
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
|
||||||
|
ImGui::BeginDisabled(isRenderLayer);
|
||||||
|
if (ImGui::Checkbox("##muted", &muted)) {
|
||||||
|
if (muted) m_layerManager->MuteLayer(li.identifier);
|
||||||
|
else m_layerManager->UnmuteLayer(li.identifier);
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
if (isRenderLayer && ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("Managed from the Render Layer panel");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Col 5: up/down reorder icon buttons.
|
||||||
|
ImGui::TableSetColumnIndex(5);
|
||||||
|
{
|
||||||
|
bool canUp = (i > 0);
|
||||||
|
bool canDown = (i < static_cast<int>(sublayers.size()) - 1);
|
||||||
|
|
||||||
|
auto buildBeforeAfter = [&](int a, int b,
|
||||||
|
std::vector<std::string>& before,
|
||||||
|
std::vector<std::string>& after) {
|
||||||
|
for (const auto& sl : sublayers) before.push_back(sl.identifier);
|
||||||
|
after = before;
|
||||||
|
std::swap(after[a], after[b]);
|
||||||
|
};
|
||||||
|
|
||||||
|
ImVec2 iconSize(14.0f, 14.0f);
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(!canUp);
|
||||||
|
ImTextureID upTex = m_iconManager ? m_iconManager->Get(Icon::ChevronUp) : ImTextureID_Invalid;
|
||||||
|
if (m_iconManager ? ImGui::ImageButton("##up", ImTextureRef(upTex), iconSize)
|
||||||
|
: ImGui::ArrowButton("##up", ImGuiDir_Up)) {
|
||||||
|
std::vector<std::string> before, after;
|
||||||
|
buildBeforeAfter(i, i - 1, before, after);
|
||||||
|
if (m_commandHistory)
|
||||||
|
m_commandHistory->Push(
|
||||||
|
std::make_unique<LayerReorderCommand>(m_layerManager, before, after));
|
||||||
|
else {
|
||||||
|
LayerReorderCommand cmd(m_layerManager, before, after);
|
||||||
|
static_cast<ICommand&>(cmd).Execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Move Up");
|
||||||
|
|
||||||
|
ImGui::SameLine(0.0f, 2.0f);
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(!canDown);
|
||||||
|
ImTextureID downTex = m_iconManager ? m_iconManager->Get(Icon::ChevronDown) : ImTextureID_Invalid;
|
||||||
|
if (m_iconManager ? ImGui::ImageButton("##down", ImTextureRef(downTex), iconSize)
|
||||||
|
: ImGui::ArrowButton("##down", ImGuiDir_Down)) {
|
||||||
|
std::vector<std::string> before, after;
|
||||||
|
buildBeforeAfter(i, i + 1, before, after);
|
||||||
|
if (m_commandHistory)
|
||||||
|
m_commandHistory->Push(
|
||||||
|
std::make_unique<LayerReorderCommand>(m_layerManager, before, after));
|
||||||
|
else {
|
||||||
|
LayerReorderCommand cmd(m_layerManager, before, after);
|
||||||
|
static_cast<ICommand&>(cmd).Execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Move Down");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SceneHierarchyPanel::RenderSublayerContextMenu(int i,
|
||||||
|
const std::vector<LayerInfo>& sublayers) {
|
||||||
|
if (!ImGui::BeginPopupContextItem(("ctx_sublayer_" + std::to_string(i)).c_str()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto& li = sublayers[i];
|
||||||
|
bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
|
||||||
|
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
|
||||||
|
|
||||||
|
if (ImGui::MenuItem(li.isMuted ? "Unmute" : "Mute", nullptr, false, !isRenderLayer)) {
|
||||||
|
if (li.isMuted) m_layerManager->UnmuteLayer(li.identifier);
|
||||||
|
else m_layerManager->MuteLayer(li.identifier);
|
||||||
|
}
|
||||||
|
if (isRenderLayer && ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("Managed from the Render Layer panel");
|
||||||
|
|
||||||
|
if (!li.isEditTarget &&
|
||||||
|
ImGui::MenuItem("Set as Edit Target", nullptr, false, !renderLayerActive))
|
||||||
|
m_layerManager->SetEditTarget(li.identifier);
|
||||||
|
if (renderLayerActive && !li.isEditTarget && ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("Edit target is locked to the active render layer");
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
if (ImGui::MenuItem("Remove", nullptr, false, !isRenderLayer)) {
|
||||||
|
if (m_commandHistory)
|
||||||
|
m_commandHistory->Push(
|
||||||
|
std::make_unique<LayerRemoveCommand>(m_layerManager, i));
|
||||||
|
else
|
||||||
|
m_layerManager->RemoveSublayer(i);
|
||||||
|
}
|
||||||
|
if (isRenderLayer && ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("Managed from the Render Layer panel");
|
||||||
|
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
|
void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
|
||||||
if (!prim.IsValid()) return;
|
if (!prim.IsValid()) return;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "../core/PropertyManager.h"
|
#include "../core/PropertyManager.h"
|
||||||
#include "../core/CommandHistory.h"
|
#include "../core/CommandHistory.h"
|
||||||
#include "../core/LayerManager.h"
|
#include "../core/LayerManager.h"
|
||||||
|
#include "../core/RenderLayerManager.h"
|
||||||
#include "IconManager.h"
|
#include "IconManager.h"
|
||||||
#include <pxr/usd/usd/stage.h>
|
#include <pxr/usd/usd/stage.h>
|
||||||
#include <pxr/usd/sdf/path.h>
|
#include <pxr/usd/sdf/path.h>
|
||||||
@@ -26,6 +27,11 @@ public:
|
|||||||
void SetPropertyManager(PropertyManager* manager);
|
void SetPropertyManager(PropertyManager* manager);
|
||||||
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
|
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
|
||||||
void SetLayerManager(LayerManager* lm) { m_layerManager = lm; }
|
void SetLayerManager(LayerManager* lm) { m_layerManager = lm; }
|
||||||
|
/// Optional. When set and a non-Default render layer is active, the
|
||||||
|
/// edit-target layer switcher below is disabled — repointing the ambient
|
||||||
|
/// edit target away from the active render layer's SdfLayer would break
|
||||||
|
/// the invariant that edits made while it's active land in that layer.
|
||||||
|
void SetRenderLayerManager(RenderLayerManager* mgr) { m_renderLayerManager = mgr; }
|
||||||
void SetStage(UsdStageRefPtr stage);
|
void SetStage(UsdStageRefPtr stage);
|
||||||
void SetIconManager(IconManager* iconManager) { m_iconManager = iconManager; }
|
void SetIconManager(IconManager* iconManager) { m_iconManager = iconManager; }
|
||||||
void Render();
|
void Render();
|
||||||
@@ -33,6 +39,12 @@ public:
|
|||||||
std::string GetSelectedPrimPath() const { return m_primarySelectedPath; }
|
std::string GetSelectedPrimPath() const { return m_primarySelectedPath; }
|
||||||
UsdPrim GetSelectedPrim() const;
|
UsdPrim GetSelectedPrim() const;
|
||||||
|
|
||||||
|
/// Full multi-selection (rect pick or Ctrl/Shift click), in no
|
||||||
|
/// particular order. Empty when nothing is selected.
|
||||||
|
std::vector<std::string> GetSelectedPaths() const {
|
||||||
|
return std::vector<std::string>(m_selectedPaths.begin(), m_selectedPaths.end());
|
||||||
|
}
|
||||||
|
|
||||||
/// Set single selection from hierarchy click (fires callback).
|
/// Set single selection from hierarchy click (fires callback).
|
||||||
/// Also clears any rect multi-selection.
|
/// Also clears any rect multi-selection.
|
||||||
void SetSelectedPathFromClick(const std::string& path);
|
void SetSelectedPathFromClick(const std::string& path);
|
||||||
@@ -79,10 +91,13 @@ private:
|
|||||||
void RenderRemovePrimModal();
|
void RenderRemovePrimModal();
|
||||||
void RenderArcModal();
|
void RenderArcModal();
|
||||||
void ProcessPendingReplaceRef();
|
void ProcessPendingReplaceRef();
|
||||||
|
void RenderSublayerSection();
|
||||||
|
void RenderSublayerContextMenu(int i, const std::vector<LayerInfo>& sublayers);
|
||||||
|
|
||||||
PropertyManager* m_propertyManager;
|
PropertyManager* m_propertyManager;
|
||||||
CommandHistory* m_commandHistory = nullptr;
|
CommandHistory* m_commandHistory = nullptr;
|
||||||
LayerManager* m_layerManager = nullptr;
|
LayerManager* m_layerManager = nullptr;
|
||||||
|
RenderLayerManager* m_renderLayerManager = nullptr;
|
||||||
IconManager* m_iconManager = nullptr;
|
IconManager* m_iconManager = nullptr;
|
||||||
UsdStageRefPtr m_stage;
|
UsdStageRefPtr m_stage;
|
||||||
|
|
||||||
@@ -94,6 +109,9 @@ private:
|
|||||||
|
|
||||||
bool m_scrollToSelected = false;
|
bool m_scrollToSelected = false;
|
||||||
|
|
||||||
|
/// Selected row in the Sublayers section (context menu / drag anchor).
|
||||||
|
int m_sublayerSelectedIdx = -1;
|
||||||
|
|
||||||
/// Stage-local layer identifiers rebuilt once per Render() for override detection.
|
/// Stage-local layer identifiers rebuilt once per Render() for override detection.
|
||||||
/// Contains only the stage's own layers (root + sublayers + session),
|
/// Contains only the stage's own layers (root + sublayers + session),
|
||||||
/// NOT layers that came in through references or payloads.
|
/// NOT layers that came in through references or payloads.
|
||||||
|
|||||||
@@ -123,13 +123,18 @@ void StageEditorPanel::RenderFixedLayers(const std::vector<LayerInfo>& layers) {
|
|||||||
|
|
||||||
ImGui::TableSetColumnIndex(3);
|
ImGui::TableSetColumnIndex(3);
|
||||||
{
|
{
|
||||||
|
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
|
||||||
bool isET = li.isEditTarget;
|
bool isET = li.isEditTarget;
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
|
||||||
|
ImGui::BeginDisabled(renderLayerActive);
|
||||||
if (ImGui::Checkbox("##et", &isET) && isET)
|
if (ImGui::Checkbox("##et", &isET) && isET)
|
||||||
m_layerManager->SetEditTarget(li.identifier);
|
m_layerManager->SetEditTarget(li.identifier);
|
||||||
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip(li.isEditTarget ? "Edit target (active)" : "Set as edit target");
|
ImGui::SetTooltip(renderLayerActive ? "Edit target is locked to the active render layer"
|
||||||
|
: li.isEditTarget ? "Edit target (active)"
|
||||||
|
: "Set as edit target");
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
@@ -242,6 +247,9 @@ void StageEditorPanel::RenderSublayerList(const std::vector<LayerInfo>& sublayer
|
|||||||
else
|
else
|
||||||
ImGui::TextDisabled(" ");
|
ImGui::TextDisabled(" ");
|
||||||
|
|
||||||
|
bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
|
||||||
|
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
|
||||||
|
|
||||||
// Col 2: layer name
|
// Col 2: layer name
|
||||||
ImGui::TableSetColumnIndex(2);
|
ImGui::TableSetColumnIndex(2);
|
||||||
ImVec4 nameColor = li.isMuted
|
ImVec4 nameColor = li.isMuted
|
||||||
@@ -250,6 +258,12 @@ void StageEditorPanel::RenderSublayerList(const std::vector<LayerInfo>& sublayer
|
|||||||
ImGui::TextColored(nameColor, "%s", li.displayName.c_str());
|
ImGui::TextColored(nameColor, "%s", li.displayName.c_str());
|
||||||
if (ImGui::IsItemHovered() && !li.realPath.empty())
|
if (ImGui::IsItemHovered() && !li.realPath.empty())
|
||||||
ImGui::SetTooltip("%s\n%s", li.identifier.c_str(), li.realPath.c_str());
|
ImGui::SetTooltip("%s\n%s", li.identifier.c_str(), li.realPath.c_str());
|
||||||
|
if (isRenderLayer) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextColored(ImVec4(0.9f, 0.7f, 0.2f, 1.0f), "[RL]");
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("Managed from the Render Layer panel");
|
||||||
|
}
|
||||||
|
|
||||||
// Col 3: edit target checkbox — checking sets this layer as edit target;
|
// Col 3: edit target checkbox — checking sets this layer as edit target;
|
||||||
// unchecking does nothing (there is always an active edit target).
|
// unchecking does nothing (there is always an active edit target).
|
||||||
@@ -257,23 +271,32 @@ void StageEditorPanel::RenderSublayerList(const std::vector<LayerInfo>& sublayer
|
|||||||
{
|
{
|
||||||
bool isET = li.isEditTarget;
|
bool isET = li.isEditTarget;
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
|
||||||
|
ImGui::BeginDisabled(renderLayerActive);
|
||||||
if (ImGui::Checkbox("##et", &isET) && isET)
|
if (ImGui::Checkbox("##et", &isET) && isET)
|
||||||
m_layerManager->SetEditTarget(li.identifier);
|
m_layerManager->SetEditTarget(li.identifier);
|
||||||
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip(li.isEditTarget ? "Edit target (active)" : "Set as edit target");
|
ImGui::SetTooltip(renderLayerActive ? "Edit target is locked to the active render layer"
|
||||||
|
: li.isEditTarget ? "Edit target (active)"
|
||||||
|
: "Set as edit target");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Col 4: mute checkbox
|
// Col 4: mute checkbox — disabled for render-layer sublayers, which
|
||||||
|
// RenderLayerManager mutes/unmutes exclusively as layers are switched.
|
||||||
ImGui::TableSetColumnIndex(4);
|
ImGui::TableSetColumnIndex(4);
|
||||||
{
|
{
|
||||||
bool muted = li.isMuted;
|
bool muted = li.isMuted;
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
|
||||||
|
ImGui::BeginDisabled(isRenderLayer);
|
||||||
if (ImGui::Checkbox("##muted", &muted)) {
|
if (ImGui::Checkbox("##muted", &muted)) {
|
||||||
if (muted) m_layerManager->MuteLayer(li.identifier);
|
if (muted) m_layerManager->MuteLayer(li.identifier);
|
||||||
else m_layerManager->UnmuteLayer(li.identifier);
|
else m_layerManager->UnmuteLayer(li.identifier);
|
||||||
}
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
|
if (isRenderLayer && ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("Managed from the Render Layer panel");
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
@@ -289,14 +312,21 @@ void StageEditorPanel::RenderSublayerContextMenu(int i,
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
const auto& li = sublayers[i];
|
const auto& li = sublayers[i];
|
||||||
|
bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
|
||||||
|
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
|
||||||
|
|
||||||
if (ImGui::MenuItem(li.isMuted ? "Unmute" : "Mute")) {
|
if (ImGui::MenuItem(li.isMuted ? "Unmute" : "Mute", nullptr, false, !isRenderLayer)) {
|
||||||
if (li.isMuted) m_layerManager->UnmuteLayer(li.identifier);
|
if (li.isMuted) m_layerManager->UnmuteLayer(li.identifier);
|
||||||
else m_layerManager->MuteLayer(li.identifier);
|
else m_layerManager->MuteLayer(li.identifier);
|
||||||
}
|
}
|
||||||
|
if (isRenderLayer && ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("Managed from the Render Layer panel");
|
||||||
|
|
||||||
if (!li.isEditTarget && ImGui::MenuItem("Set as Edit Target"))
|
if (!li.isEditTarget &&
|
||||||
|
ImGui::MenuItem("Set as Edit Target", nullptr, false, !renderLayerActive))
|
||||||
m_layerManager->SetEditTarget(li.identifier);
|
m_layerManager->SetEditTarget(li.identifier);
|
||||||
|
if (renderLayerActive && !li.isEditTarget && ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("Edit target is locked to the active render layer");
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
@@ -337,13 +367,15 @@ void StageEditorPanel::RenderSublayerContextMenu(int i,
|
|||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
if (ImGui::MenuItem("Remove")) {
|
if (ImGui::MenuItem("Remove", nullptr, false, !isRenderLayer)) {
|
||||||
if (m_commandHistory)
|
if (m_commandHistory)
|
||||||
m_commandHistory->Push(
|
m_commandHistory->Push(
|
||||||
std::make_unique<LayerRemoveCommand>(m_layerManager, i));
|
std::make_unique<LayerRemoveCommand>(m_layerManager, i));
|
||||||
else
|
else
|
||||||
m_layerManager->RemoveSublayer(i);
|
m_layerManager->RemoveSublayer(i);
|
||||||
}
|
}
|
||||||
|
if (isRenderLayer && ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("Managed from the Render Layer panel");
|
||||||
|
|
||||||
ImGui::EndPopup();
|
ImGui::EndPopup();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "../core/LayerManager.h"
|
#include "../core/LayerManager.h"
|
||||||
#include "../core/CommandHistory.h"
|
#include "../core/CommandHistory.h"
|
||||||
|
#include "../core/RenderLayerManager.h"
|
||||||
#include "IconManager.h"
|
#include "IconManager.h"
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -17,6 +18,12 @@ public:
|
|||||||
void SetLayerManager(LayerManager* manager) { m_layerManager = manager; }
|
void SetLayerManager(LayerManager* manager) { m_layerManager = manager; }
|
||||||
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
|
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
|
||||||
void SetIconManager(IconManager* icons) { m_iconManager = icons; }
|
void SetIconManager(IconManager* icons) { m_iconManager = icons; }
|
||||||
|
/// Optional. When set: sublayers RenderLayerManager created are badged
|
||||||
|
/// and have their mute/remove controls disabled here (they're managed
|
||||||
|
/// exclusively from the Render Layer panel so the two can't desync), and
|
||||||
|
/// every row's edit-target checkbox is disabled while a non-Default
|
||||||
|
/// render layer is active.
|
||||||
|
void SetRenderLayerManager(RenderLayerManager* mgr) { m_renderLayerManager = mgr; }
|
||||||
void Render();
|
void Render();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -27,6 +34,7 @@ private:
|
|||||||
LayerManager* m_layerManager = nullptr;
|
LayerManager* m_layerManager = nullptr;
|
||||||
CommandHistory* m_commandHistory = nullptr;
|
CommandHistory* m_commandHistory = nullptr;
|
||||||
IconManager* m_iconManager = nullptr;
|
IconManager* m_iconManager = nullptr;
|
||||||
|
RenderLayerManager* m_renderLayerManager = nullptr;
|
||||||
int m_selectedIdx = -1;
|
int m_selectedIdx = -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ struct ViewportTileSettings {
|
|||||||
bool ambientLightOnly = true;
|
bool ambientLightOnly = true;
|
||||||
bool domeLightEnabled = false;
|
bool domeLightEnabled = false;
|
||||||
int shadingMode = 0;
|
int shadingMode = 0;
|
||||||
int cullStyle = 4; // CullStyle::BackUnlessDoubleSided
|
// CullStyle::Nothing — matches usdview's default (viewSettingsDataModel
|
||||||
|
// cullBackfaces=False -> CULL_STYLE_NOTHING). BackUnlessDoubleSided drops
|
||||||
|
// back faces on geometry that isn't authored doubleSided, which hdEmbree
|
||||||
|
// applies to occlusion rays as well, so interiors shade as single-sided.
|
||||||
|
int cullStyle = 1; // CullStyle::Nothing
|
||||||
bool showGuides = false;
|
bool showGuides = false;
|
||||||
bool showProxy = true;
|
bool showProxy = true;
|
||||||
bool showRender = false;
|
bool showRender = false;
|
||||||
|
|||||||