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
+26 -21
View File
@@ -14,6 +14,7 @@
#include <pxr/usd/usdLux/tokens.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/usd/references.h>
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/sdf/assetPath.h>
#include <pxr/usd/sdf/primSpec.h>
#include <imgui.h>
@@ -48,6 +49,10 @@ void MaterialPreviewRenderer::EnsureInitialized() {
if (m_initialized) return;
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
// rig, dome-pole compensation, and camera framing all assume Y-up.
pxr::UsdGeomSetStageUpAxis(m_previewStage, pxr::UsdGeomTokens->y);
@@ -112,14 +117,18 @@ void MaterialPreviewRenderer::SetMaterial(const pxr::UsdStageRefPtr& sourceStage
// materials can be spread across sublayers. Referencing at the identical
// path keeps internal connection paths valid without remapping, and
// relative asset paths keep resolving against their original layers.
pxr::UsdPrim mirrorPrim = m_previewStage->OverridePrim(materialPath);
if (!mirrorPrim) {
LOG_ERROR("MaterialPreviewRenderer: failed to create override at " + materialPath.GetString());
return;
// 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);
if (!mirrorPrim) {
LOG_ERROR("MaterialPreviewRenderer: failed to create override at " + materialPath.GetString());
return;
}
mirrorPrim.GetReferences().ClearReferences();
mirrorPrim.GetReferences().AddReference(
sourceStage->GetRootLayer()->GetIdentifier(), materialPath);
}
mirrorPrim.GetReferences().ClearReferences();
mirrorPrim.GetReferences().AddReference(
sourceStage->GetRootLayer()->GetIdentifier(), materialPath);
pxr::UsdPrim spherePrim = m_previewStage->GetPrimAtPath(kSpherePath);
pxr::UsdShadeMaterial material(m_previewStage->GetPrimAtPath(materialPath));
@@ -131,20 +140,14 @@ void MaterialPreviewRenderer::SetMaterial(const pxr::UsdStageRefPtr& sourceStage
pxr::UsdShadeMaterialBindingAPI::Apply(spherePrim).Bind(material);
// ── Selected-node preview (Hypershade-style) ─────────────────────────
// Wipe any override left by a previous selection: the scratch wrapper
// shader and the local opinions on the material's surface outputs, all
// of which live only in the scratch stage's root layer.
pxr::SdfLayerHandle rootLayer = m_previewStage->GetRootLayer();
if (pxr::SdfPrimSpecHandle matSpec = rootLayer->GetPrimAtPath(materialPath)) {
if (pxr::SdfPrimSpecHandle wrapSpec =
rootLayer->GetPrimAtPath(materialPath.AppendChild(kNodePreviewShaderName)))
matSpec->RemoveNameChild(wrapSpec);
for (const char* outName : {"outputs:surface", "outputs:mtlx:surface"}) {
if (pxr::SdfPropertySpecHandle prop =
rootLayer->GetPropertyAtPath(materialPath.AppendProperty(pxr::TfToken(outName))))
matSpec->RemoveProperty(prop);
}
}
// All node-preview overrides (the scratch wrapper shader and the local
// opinions on the material's surface outputs) live exclusively in the
// preview stage's session layer, which is stronger than the root layer's
// reference. Wiping them is then just clearing that layer — no per-spec
// surgery (RemoveNameChild/RemoveProperty) that could interact badly with
// Sdf's inert-spec cleanup, and no stale overrides on other materials.
pxr::SdfLayerHandle sessionLayer = m_previewStage->GetSessionLayer();
sessionLayer->Clear();
// Route the material's surface through the selected node. Nodes outside
// 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)) {
pxr::UsdShadeShader nodeShader(m_previewStage->GetPrimAtPath(previewNodePath));
if (nodeShader) {
pxr::UsdEditContext sessionEdit(m_previewStage,
pxr::UsdEditTarget(sessionLayer));
pxr::UsdShadeConnectableAPI sourceApi = nodeShader.ConnectableAPI();
pxr::TfToken sourceName(previewNodeOutput);
if (!previewOutputIsTerminal) {