Add Maya-style Render Layer panel

Render layers built on the existing USD sublayer architecture: each layer is a
file-backed SdfLayer mounted as a sublayer and tagged via custom layer data,
with membership stored in a UsdCollectionAPI on a hidden /RenderLayerData prim.
Activating a layer applies its overrides and isolates it to its members.
"Default" is virtual -- it means all render-layer sublayers muted.

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 10:02:33 +08:00
parent e2a4961ebe
commit b5c1e50438
19 changed files with 1720 additions and 35 deletions
+45
View File
@@ -41,6 +41,7 @@ Application::Application()
: m_showDemoWindow(false)
, m_showStageInfo(true)
, m_showStageEditor(true)
, m_showRenderLayerPanel(true)
, m_showSceneHierarchy(true)
, m_showViewport(true)
, m_showPropertyPanel(true)
@@ -67,15 +68,23 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
// Create managers
m_stageManager = std::make_unique<UsdStageManager>();
m_layerManager = std::make_unique<LayerManager>();
m_renderLayerManager = std::make_unique<RenderLayerManager>();
m_renderLayerManager->SetLayerManager(m_layerManager.get());
m_propertyManager = std::make_unique<PropertyManager>();
m_propertyManager->SetCommandHistory(&m_commandHistory);
m_stageEditorPanel = std::make_unique<StageEditorPanel>();
m_stageEditorPanel->SetLayerManager(m_layerManager.get());
m_stageEditorPanel->SetCommandHistory(&m_commandHistory);
m_stageEditorPanel->SetRenderLayerManager(m_renderLayerManager.get());
m_sceneHierarchyPanel = std::make_unique<SceneHierarchyPanel>();
m_sceneHierarchyPanel->SetPropertyManager(m_propertyManager.get());
m_sceneHierarchyPanel->SetCommandHistory(&m_commandHistory);
m_sceneHierarchyPanel->SetLayerManager(m_layerManager.get());
m_sceneHierarchyPanel->SetRenderLayerManager(m_renderLayerManager.get());
m_renderLayerPanel = std::make_unique<RenderLayerPanel>();
m_renderLayerPanel->SetRenderLayerManager(m_renderLayerManager.get());
m_renderLayerPanel->SetCommandHistory(&m_commandHistory);
m_renderLayerPanel->SetSceneHierarchyPanel(m_sceneHierarchyPanel.get());
m_viewportPanel = std::make_unique<ViewportPanel>();
m_viewportPanel->SetCommandHistory(&m_commandHistory);
m_propertyPanel = std::make_unique<PropertyPanel>();
@@ -116,6 +125,7 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
m_timelinePanel->SetIconManager(m_iconManager.get());
m_stageEditorPanel->SetIconManager(m_iconManager.get());
m_materialEditorPanel->SetIconManager(m_iconManager.get());
m_renderLayerPanel->SetIconManager(m_iconManager.get());
m_sceneHierarchyPanel->SetOnPrimSelected(
[this](const std::string& path) {
@@ -235,10 +245,12 @@ void Application::Shutdown() {
m_sceneHierarchyPanel.reset();
m_propertyPanel.reset();
m_stageEditorPanel.reset();
m_renderLayerPanel.reset();
// Owns the shader-ball preview's Hydra engine + GL draw target — must be
// destroyed while the GL context still exists, like m_viewportPanel.
m_materialEditorPanel.reset();
m_propertyManager.reset();
m_renderLayerManager.reset();
m_layerManager.reset();
if (m_iconManager) {
@@ -263,8 +275,12 @@ void Application::RefreshManagers() {
if (m_stageManager->HasStage()) {
auto stage = m_stageManager->GetStage();
m_layerManager->SetStage(stage);
// After LayerManager (ActivateRenderLayer routes edit-target changes
// through it) but before RestorePersistedActiveLayer needs it.
m_renderLayerManager->SetStage(stage);
m_propertyManager->SetStage(stage);
m_sceneHierarchyPanel->SetStage(stage);
m_renderLayerPanel->SetStage(stage);
m_viewportPanel->SetStage(stage);
m_viewportPanel->FrameScene();
m_propertyPanel->SetStage(stage);
@@ -272,10 +288,16 @@ void Application::RefreshManagers() {
m_curveEditorPanel->SetStage(stage);
m_materialManager->SetStage(stage);
m_materialEditorPanel->SetStage(stage);
// Re-applies whichever render layer was active last time this stage
// was saved (mute state itself isn't serialized — see
// RenderLayerManager::RestorePersistedActiveLayer).
m_renderLayerManager->RestorePersistedActiveLayer();
} else {
m_layerManager->SetStage(nullptr);
m_renderLayerManager->SetStage(nullptr);
m_propertyManager->SetStage(nullptr);
m_sceneHierarchyPanel->SetStage(nullptr);
m_renderLayerPanel->SetStage(nullptr);
m_viewportPanel->SetStage(nullptr);
m_propertyPanel->SetStage(nullptr);
m_timelinePanel->SetStage(nullptr);
@@ -327,6 +349,12 @@ void Application::RenderUI() {
ImGui::End();
}
if (m_showRenderLayerPanel) {
ImGui::Begin("Render Layers", &m_showRenderLayerPanel, ImGuiWindowFlags_NoCollapse);
m_renderLayerPanel->Render();
ImGui::End();
}
if (m_showViewport) {
m_viewportPanel->Render(&m_showViewport);
}
@@ -777,6 +805,7 @@ void Application::RenderMenuBar() {
if (ImGui::BeginMenu("View")) {
ImGui::MenuItem("Stage Editor", nullptr, &m_showStageEditor);
ImGui::MenuItem("Render Layers", nullptr, &m_showRenderLayerPanel);
ImGui::MenuItem("Scene Hierarchy", nullptr, &m_showSceneHierarchy);
ImGui::MenuItem("Viewport", nullptr, &m_showViewport);
ImGui::MenuItem("Property Panel", nullptr, &m_showPropertyPanel);
@@ -872,7 +901,9 @@ void Application::SaveUsdFile() {
if (!m_stageManager->SaveStage()) {
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
return;
}
SaveDirtyRenderLayers();
}
void Application::SaveUsdFileAs() {
@@ -891,6 +922,11 @@ void Application::SaveUsdFileAs() {
if (!m_stageManager->SaveStageAs(filePath)) {
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
} else {
// m_renderLayerManager's layer refs are identifier-keyed SdfLayer
// objects, independent of which UsdStage currently references
// them — safe to save before the stage swap RefreshManagers()
// below performs.
SaveDirtyRenderLayers();
// SaveStageAs reopens m_stageManager's stage from the new file path.
// RefreshManagers syncs m_layerManager (and others) to that new stage;
// without this, subsequent sublayer edits go to the old (now stale) stage
@@ -900,6 +936,15 @@ void Application::SaveUsdFileAs() {
}
}
void Application::SaveDirtyRenderLayers() {
if (!m_renderLayerManager) return;
for (const auto& info : m_renderLayerManager->GetRenderLayers()) {
if (info.isDefault || !info.layer) continue;
if (info.layer->IsDirty())
info.layer->Save();
}
}
void Application::CloseUsdFile() {
m_stageManager->CloseStage();
// Re-create a fresh default stage so the app is always in an editable state.