Fix node thumbnails not converging with progressive renderers; add colorSpace and Sdr-enum widgets

- NodeThumbnailCache/MaterialPreviewRenderer: keep a material node
  thumbnail stale (re-pumped each frame) until the shared preview
  renderer actually converges, instead of caching after one render
  pass. Progressive delegates (Arnold/Cycles/Embree) need multiple
  passes to accumulate past their first noisy sample, so thumbnails
  previously froze on an early frame and only "fixed themselves"
  when toggling thumbnail display forced a fresh render.
- MaterialEditorPanel: add a Color Space row under Asset-typed
  shader inputs, authoring the attribute's colorSpace metadata from
  the active OCIO config's color spaces.
- MaterialManager: surface Sdr-declared allowed values (enums) per
  shader input so the property panel can render a dropdown instead
  of free text.
- CMakePresets.json: build the release preset with debug info
  (RelWithDebInfo) for easier diagnosis.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 17:39:14 +08:00
parent 6cb1b45496
commit 196a2bb0dd
9 changed files with 227 additions and 15 deletions
+119 -5
View File
@@ -8,6 +8,7 @@
#include "../core/commands/RenamePrimCommand.h"
#include "../utils/Logger.h"
#include "../utils/FileDialog.h"
#include "../utils/OcioConfigParser.h"
#include <pxr/usd/usdShade/shader.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
@@ -365,6 +366,14 @@ void MaterialEditorPanel::SetColorCorrectionFromPrefs(int ccMode, const std::str
const std::string& ocioColorSpace,
const std::string& 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() {
@@ -570,6 +579,18 @@ void MaterialEditorPanel::RenderSelectedNodeProperties() {
RenderInputValueWidget(*node, input, prim);
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::EndTable();
@@ -645,11 +666,39 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
std::string s;
if (current.IsHolding<std::string>()) s = current.UncheckedGet<std::string>();
else if (current.IsHolding<pxr::TfToken>()) s = current.UncheckedGet<pxr::TfToken>().GetString();
char buf[512];
std::snprintf(buf, sizeof(buf), "%s", s.c_str());
if (ImGui::InputText(widgetId.c_str(), buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) {
if (type == tn->String) newValue = std::string(buf);
else newValue = pxr::TfToken(buf);
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];
std::snprintf(buf, sizeof(buf), "%s", s.c_str());
if (ImGui::InputText(widgetId.c_str(), buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) {
if (type == tn->String) newValue = std::string(buf);
else newValue = pxr::TfToken(buf);
}
}
} else if (type == tn->Asset) {
std::string s;
@@ -715,6 +764,64 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
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) {
if (!m_stage || !m_commandHistory) return;
@@ -1418,6 +1525,13 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
const ImVec2 viewCenterScreen(regionPos.x + regionSize.x * 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
// thumbnail, and prune dead entries — GL/FBO work kept outside the
// node-editor's Begin/End so it never touches mid-draw ImGui state.