Author shader-ball node-preview overrides in the session layer

Fixes 'No spec at </Materials/...> when trying to set field
primChildren' during pin edits: the per-revision wipe used raw spec
surgery (RemoveNameChild/RemoveProperty) plus a ClearReferences/
AddReference cycle that briefly leaves the material's over spec empty
and inert, which Sdf cleanup can delete out from under later authoring.

The reference to the source material is now authored only when the
material changes (shared in-memory layers recompose live), and all
node-preview overrides live in the preview stage's session layer,
wiped wholesale with SdfLayer::Clear — no spec-level removal remains,
and switching materials no longer leaves stale overrides behind. The
edit target is pinned to the root layer so the rig never lands in the
cleared session layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 23:09:05 +08:00
parent b08bb6b233
commit bb9a5d9428
+19 -14
View File
@@ -14,6 +14,7 @@
#include <pxr/usd/usdLux/tokens.h> #include <pxr/usd/usdLux/tokens.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h> #include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/usd/references.h> #include <pxr/usd/usd/references.h>
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/sdf/assetPath.h> #include <pxr/usd/sdf/assetPath.h>
#include <pxr/usd/sdf/primSpec.h> #include <pxr/usd/sdf/primSpec.h>
#include <imgui.h> #include <imgui.h>
@@ -48,6 +49,10 @@ void MaterialPreviewRenderer::EnsureInitialized() {
if (m_initialized) return; if (m_initialized) return;
m_previewStage = pxr::UsdStage::CreateInMemory(); m_previewStage = pxr::UsdStage::CreateInMemory();
// Everything authored here (sphere, lights, reference) must go to the
// root layer: the session layer is reserved for node-preview overrides
// and is wiped wholesale on every SetMaterial.
m_previewStage->SetEditTarget(pxr::UsdEditTarget(m_previewStage->GetRootLayer()));
// Pin the up axis rather than inheriting the site fallback: the light // Pin the up axis rather than inheriting the site fallback: the light
// rig, dome-pole compensation, and camera framing all assume Y-up. // rig, dome-pole compensation, and camera framing all assume Y-up.
pxr::UsdGeomSetStageUpAxis(m_previewStage, pxr::UsdGeomTokens->y); pxr::UsdGeomSetStageUpAxis(m_previewStage, pxr::UsdGeomTokens->y);
@@ -112,6 +117,9 @@ void MaterialPreviewRenderer::SetMaterial(const pxr::UsdStageRefPtr& sourceStage
// materials can be spread across sublayers. Referencing at the identical // materials can be spread across sublayers. Referencing at the identical
// path keeps internal connection paths valid without remapping, and // path keeps internal connection paths valid without remapping, and
// relative asset paths keep resolving against their original layers. // relative asset paths keep resolving against their original layers.
// Authored only when the material changes: the shared in-memory layers
// recompose live, so revision bumps need no re-referencing.
if (materialPath != m_materialPath) {
pxr::UsdPrim mirrorPrim = m_previewStage->OverridePrim(materialPath); pxr::UsdPrim mirrorPrim = m_previewStage->OverridePrim(materialPath);
if (!mirrorPrim) { if (!mirrorPrim) {
LOG_ERROR("MaterialPreviewRenderer: failed to create override at " + materialPath.GetString()); LOG_ERROR("MaterialPreviewRenderer: failed to create override at " + materialPath.GetString());
@@ -120,6 +128,7 @@ void MaterialPreviewRenderer::SetMaterial(const pxr::UsdStageRefPtr& sourceStage
mirrorPrim.GetReferences().ClearReferences(); mirrorPrim.GetReferences().ClearReferences();
mirrorPrim.GetReferences().AddReference( mirrorPrim.GetReferences().AddReference(
sourceStage->GetRootLayer()->GetIdentifier(), materialPath); sourceStage->GetRootLayer()->GetIdentifier(), materialPath);
}
pxr::UsdPrim spherePrim = m_previewStage->GetPrimAtPath(kSpherePath); pxr::UsdPrim spherePrim = m_previewStage->GetPrimAtPath(kSpherePath);
pxr::UsdShadeMaterial material(m_previewStage->GetPrimAtPath(materialPath)); pxr::UsdShadeMaterial material(m_previewStage->GetPrimAtPath(materialPath));
@@ -131,20 +140,14 @@ void MaterialPreviewRenderer::SetMaterial(const pxr::UsdStageRefPtr& sourceStage
pxr::UsdShadeMaterialBindingAPI::Apply(spherePrim).Bind(material); pxr::UsdShadeMaterialBindingAPI::Apply(spherePrim).Bind(material);
// ── Selected-node preview (Hypershade-style) ───────────────────────── // ── Selected-node preview (Hypershade-style) ─────────────────────────
// Wipe any override left by a previous selection: the scratch wrapper // All node-preview overrides (the scratch wrapper shader and the local
// shader and the local opinions on the material's surface outputs, all // opinions on the material's surface outputs) live exclusively in the
// of which live only in the scratch stage's root layer. // preview stage's session layer, which is stronger than the root layer's
pxr::SdfLayerHandle rootLayer = m_previewStage->GetRootLayer(); // reference. Wiping them is then just clearing that layer — no per-spec
if (pxr::SdfPrimSpecHandle matSpec = rootLayer->GetPrimAtPath(materialPath)) { // surgery (RemoveNameChild/RemoveProperty) that could interact badly with
if (pxr::SdfPrimSpecHandle wrapSpec = // Sdf's inert-spec cleanup, and no stale overrides on other materials.
rootLayer->GetPrimAtPath(materialPath.AppendChild(kNodePreviewShaderName))) pxr::SdfLayerHandle sessionLayer = m_previewStage->GetSessionLayer();
matSpec->RemoveNameChild(wrapSpec); sessionLayer->Clear();
for (const char* outName : {"outputs:surface", "outputs:mtlx:surface"}) {
if (pxr::SdfPropertySpecHandle prop =
rootLayer->GetPropertyAtPath(materialPath.AppendProperty(pxr::TfToken(outName))))
matSpec->RemoveProperty(prop);
}
}
// Route the material's surface through the selected node. Nodes outside // Route the material's surface through the selected node. Nodes outside
// the material's subtree (possible in hand-authored cross-scope networks) // the material's subtree (possible in hand-authored cross-scope networks)
@@ -154,6 +157,8 @@ void MaterialPreviewRenderer::SetMaterial(const pxr::UsdStageRefPtr& sourceStage
previewNodePath.HasPrefix(materialPath)) { previewNodePath.HasPrefix(materialPath)) {
pxr::UsdShadeShader nodeShader(m_previewStage->GetPrimAtPath(previewNodePath)); pxr::UsdShadeShader nodeShader(m_previewStage->GetPrimAtPath(previewNodePath));
if (nodeShader) { if (nodeShader) {
pxr::UsdEditContext sessionEdit(m_previewStage,
pxr::UsdEditTarget(sessionLayer));
pxr::UsdShadeConnectableAPI sourceApi = nodeShader.ConnectableAPI(); pxr::UsdShadeConnectableAPI sourceApi = nodeShader.ConnectableAPI();
pxr::TfToken sourceName(previewNodeOutput); pxr::TfToken sourceName(previewNodeOutput);
if (!previewOutputIsTerminal) { if (!previewOutputIsTerminal) {