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:
@@ -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」
|
||||
——以追加的方式記錄後續進展或設計變動,而非直接覆寫、抹除本次
|
||||
決策當下的紀錄。
|
||||
Reference in New Issue
Block a user