Files
UsdLayerManager/src/core/UsdSceneRenderer.cpp
T
indigo 09819091c4 Add Hypershade-style Material Editor with node graph and shader-ball preview
Browser column lists all scene materials plus a searchable create-node
list; Show Graph (or double-click) loads a material into the node-graph
work area. The graph shows the entire network including MaterialX
(.mtlx) node graphs, resolving connections through NodeGraph boundaries
via UsdShadeUtils::GetValueProducingAttributes, with layered auto-layout
for nodes lacking authored uiPosition. Canvas navigates viewport-style
(Alt+MMB pan / Alt+RMB zoom) and TAB opens a Nuke-style search popup.
Selecting a node shows a typed property editor (live-apply, one undo
command per edit) and previews that node's output on the shader ball.

The preview renders through its own Hydra engine into a scratch stage
that composes the material via a reference to the source root layer (so
referenced .mtlx materials work), re-renders until progressive delegates
(Arnold/Cycles/Embree) converge, and lights with HDR dome presets
(External/Room/Interior/Sunset; CC0 Poly Haven EXRs fetched at CMake
configure). texture:format is authored latlong explicitly - left
automatic, hdArnold falls back to Arnold's angular fisheye default -
and hdCycles gets a -90 X pole rotation to match Storm's +Y-pole
sampling.

Mutations go through new ICommand subclasses (create shader node,
connect/disconnect attrs). imgui-node-editor is vendored (gitignored)
with a local one-line patch: c_ScrollButtonIndex 1->2 for MMB pan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 20:44:54 +08:00

1915 lines
74 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "UsdSceneRenderer.h"
#include "../utils/Logger.h"
#include "../utils/GLExt.h"
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usdGeom/metrics.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdGeom/bboxCache.h>
#include <pxr/usd/usdGeom/camera.h>
#include <pxr/usd/usdGeom/xformCache.h>
#include <pxr/usd/usdLux/lightAPI.h>
#include <pxr/usd/usdLux/sphereLight.h>
#include <pxr/usd/usdLux/rectLight.h>
#include <pxr/usd/usdLux/diskLight.h>
#include <pxr/usd/usdLux/distantLight.h>
#include <pxr/usd/usdLux/domeLight.h>
#include <pxr/usd/usdLux/cylinderLight.h>
#include <pxr/imaging/glf/contextCaps.h>
#include <pxr/imaging/cameraUtil/conformWindow.h>
#include <pxr/imaging/cameraUtil/framing.h>
#include <pxr/base/gf/range2f.h>
#include <pxr/base/gf/rect2i.h>
#include <pxr/base/gf/frustum.h>
#include <pxr/base/gf/rotation.h>
#include <pxr/base/tf/diagnosticMgr.h>
#include <pxr/base/tf/error.h>
#include <pxr/base/tf/warning.h>
#include <pxr/base/tf/status.h>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <exception>
#include <unordered_set>
namespace UsdLayerManager {
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static std::string Vec3fStr(const pxr::GfVec3f& v) {
return "(" + std::to_string(v[0]) + ", " + std::to_string(v[1]) + ", "
+ std::to_string(v[2]) + ")";
}
// Forwards USD's TfError / TF_WARN / TF_STATUS messages into the app log.
// Without this, Hydra diagnostics (e.g. the reason HdxColorCorrectionTask
// fails to build an OCIO processor) only go to a stderr console the GUI
// never shows. Installed once, process-wide, from InitRenderer().
namespace {
class UsdDiagnosticLogger : public pxr::TfDiagnosticMgr::Delegate {
public:
void IssueError(pxr::TfError const& err) override {
LOG_ERROR("[USD] " + err.GetCommentary());
}
void IssueFatalError(pxr::TfCallContext const&,
std::string const& msg) override {
LOG_ERROR("[USD fatal] " + msg);
}
void IssueStatus(pxr::TfStatus const& status) override {
LOG_INFO("[USD] " + status.GetCommentary());
}
void IssueWarning(pxr::TfWarning const& warning) override {
LOG_WARNING("[USD] " + warning.GetCommentary());
}
};
void InstallUsdDiagnosticLogger() {
static UsdDiagnosticLogger s_logger; // process-lifetime; never removed
static bool s_installed = false;
if (s_installed) return;
s_installed = true;
pxr::TfDiagnosticMgr::GetInstance().AddDelegate(&s_logger);
}
} // namespace
/// Port of stageView._ComputeCameraFraming():
/// Converts a Y-up integer viewport rect into a CameraUtilFraming whose
/// display/data windows are expressed in the Y-down coordinate system that
/// CameraUtilFraming / OpenEXR use.
static pxr::CameraUtilFraming ComputeCameraFraming(
int x, int y, int w, int h,
int renderBufferWidth, int renderBufferHeight)
{
// Flip Y: viewport is Y-up, display/data windows are Y-down.
float dy = static_cast<float>(renderBufferHeight - y - h);
float dy2 = static_cast<float>(renderBufferHeight - y);
pxr::GfRange2f displayWindow(
pxr::GfVec2f(static_cast<float>(x), dy),
pxr::GfVec2f(static_cast<float>(x + w), dy2));
// dataWindow: integer rect, same area but Y-flipped.
pxr::GfRect2i renderBufferRect(
pxr::GfVec2i(0, 0),
renderBufferWidth, renderBufferHeight);
pxr::GfRect2i dataWindow = renderBufferRect.GetIntersection(
pxr::GfRect2i(
pxr::GfVec2i(x, static_cast<int>(dy)),
w, h));
return pxr::CameraUtilFraming(displayWindow, dataWindow);
}
static GLuint CompileShader(const char* source, GLenum type) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint ok = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[512];
glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
LOG_ERROR("Shader compile error: " + std::string(log));
glDeleteShader(shader);
return 0;
}
return shader;
}
static GLuint LinkProgram(GLuint vs, GLuint fs) {
GLuint prog = glCreateProgram();
glAttachShader(prog, vs);
glAttachShader(prog, fs);
glLinkProgram(prog);
glDeleteShader(vs);
glDeleteShader(fs);
GLint ok = 0;
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
if (!ok) {
char log[512];
glGetProgramInfoLog(prog, sizeof(log), nullptr, log);
LOG_ERROR("Program link error: " + std::string(log));
glDeleteProgram(prog);
return 0;
}
return prog;
}
// ---------------------------------------------------------------------------
// GLSL sources
// ---------------------------------------------------------------------------
// The grid reuses the same axis shader (kAxisVS / kAxisFS) — no separate
// grid program is needed. Line geometry is built in RebuildGridVBO().
// Axis shader (stageView.DrawAxis port)
// VS receives 3D position; uniform MVP scales and projects.
static const char* kAxisVS = R"(#version 130
in vec3 position;
uniform mat4 mvpMatrix;
void main() { gl_Position = vec4(position, 1.0) * mvpMatrix; }
)";
static const char* kAxisFS = R"(#version 130
uniform vec4 color;
out vec4 outColor;
void main() { outColor = color; }
)";
// ===========================================================================
// Constructor / Destructor
// ===========================================================================
UsdSceneRenderer::UsdSceneRenderer()
: m_showGrid(true)
, m_showCameraGuide(false)
, m_showGuides(false)
, m_showProxy(true)
, m_showRender(false)
, m_currentAov(pxr::TfToken("color"))
, m_aaEnabled(false)
, m_backgroundColor(0.15f, 0.15f, 0.15f)
, m_shadingMode(ShadingMode::SmoothShaded)
, m_cullStyle(CullStyle::BackUnlessDoubleSided)
, m_colorCorrectionMode(ColorCorrectionMode::sRGB)
, m_ambientLightOnly(true)
, m_domeLightEnabled(false)
, m_stageIsZup(false)
, m_defaultMaterialAmbient(0.2f)
, m_defaultMaterialSpecular(0.1f)
, m_rendererInitialized(false)
, m_forceRefresh(false)
, m_useCameraPath(false)
, m_diagFrameCount(0)
, m_gridVAO(0)
, m_gridVBO(0)
, m_gridMinorFirst(0), m_gridMinorCount(0)
, m_gridMajorFirst(0), m_gridMajorCount(0)
, m_gridAxisAFirst(0), m_gridAxisACount(0)
, m_gridAxisBFirst(0), m_gridAxisBCount(0)
, m_gridHalfSize(50.0f)
, m_axisVAO(0)
, m_axisVBO(0)
, m_axisProgram(0)
, m_axisUniformMVP(-1)
, m_axisUniformColor(-1)
, m_bboxMode(BBoxMode::None)
, m_bboxColor(1.0f, 1.0f, 1.0f, 1.0f)
, m_bboxVAO(0)
, m_bboxVBO(0)
, m_bboxProgram(0)
, m_bboxUniformMVP(-1)
, m_bboxUniformColor(-1)
{
m_viewMatrix.SetIdentity();
m_projMatrix.SetIdentity();
}
UsdSceneRenderer::~UsdSceneRenderer() {
DestroyGridResources();
DestroyAxisResources();
DestroyBBoxResources();
DestroyCamWireResources();
DestroyLightWireResources();
}
// ===========================================================================
// Stage
// ===========================================================================
void UsdSceneRenderer::SetStage(pxr::UsdStageRefPtr stage) {
m_stage = stage;
m_rendererInitialized = false;
m_useCameraPath = false;
m_cameraPath = pxr::SdfPath();
m_clipPlanes.clear();
m_cameraCacheDirty = true;
DestroyGridResources();
DestroyAxisResources();
DestroyBBoxResources();
DestroyCamWireResources();
DestroyLightWireResources();
// Determine stage up-axis for dome light rotation (mirrors stageView._stageIsZup)
if (stage) {
pxr::TfToken upAxis = pxr::UsdGeomGetStageUpAxis(stage);
m_stageIsZup = (upAxis == pxr::UsdGeomTokens->z);
} else {
m_stageIsZup = false;
}
// Rebuild grid geometry for the new up-axis (if GL resources are ready)
RebuildGridVBO();
}
// ===========================================================================
// Renderer Initialization (lazy, deferred to first Render())
// ===========================================================================
void UsdSceneRenderer::InitRenderer() {
if (m_rendererInitialized || !m_stage) return;
LOG_INFO("UsdSceneRenderer::InitRenderer - initializing...");
// Route USD/Hydra diagnostics (incl. OCIO failures) to the app log.
InstallUsdDiagnosticLogger();
pxr::GlfContextCaps::InitInstance();
pxr::UsdImagingGLEngine::Parameters params;
params.rootPath = m_stage->GetPseudoRoot().GetPath();
params.excludedPaths = {};
m_renderer = std::make_shared<pxr::UsdImagingGLEngine>(params);
if (!m_renderer) {
LOG_ERROR("Failed to create UsdImagingGLEngine");
return;
}
// Plugin selection: prefer HdStorm / GL-based renderers
auto plugins = pxr::UsdImagingGLEngine::GetRendererPlugins();
LOG_INFO("Available renderer plugins: " + std::to_string(plugins.size()));
bool hasCycles = false;
for (const auto& p : plugins) {
LOG_INFO(" " + std::string(p.GetText()) + " -> "
+ pxr::UsdImagingGLEngine::GetRendererDisplayName(p));
if (std::string(p.GetText()) == "HdCyclesPlugin") hasCycles = true;
}
if (!hasCycles)
LOG_WARNING("HdCyclesPlugin not available (see startup log for DLL load errors).");
// Apply the requested plugin. m_currentRendererPlugin is set from global
// preferences before InitRenderer runs; always honour it rather than
// checking whether the engine already chose something by default.
if (!m_currentRendererPlugin.IsEmpty()) {
pxr::TfToken cur = m_renderer->GetCurrentRendererId();
if (cur != m_currentRendererPlugin)
m_renderer->SetRendererPlugin(m_currentRendererPlugin);
} else if (!plugins.empty()) {
// Fallback heuristic: prefer Storm/GL
pxr::TfToken best;
for (const auto& p : plugins) {
std::string n(p.GetText());
if (n.find("Storm") != std::string::npos ||
n.find("GL") != std::string::npos) { best = p; break; }
}
if (best.IsEmpty()) best = plugins[0];
m_renderer->SetRendererPlugin(best);
}
LOG_INFO("Selected renderer: " + std::string(m_renderer->GetCurrentRendererId().GetText()));
// Enable AOV (matches stageView._handleRendererChanged; uses saved m_currentAov)
m_renderer->SetRendererAov(m_currentAov);
// Selection highlight color (usdview default: yellow)
m_renderer->SetSelectionColor(pxr::GfVec4f(1.0f, 1.0f, 0.0f, 1.0f));
// Remember which plugin is active
m_currentRendererPlugin = m_renderer->GetCurrentRendererId();
m_rendererInitialized = true;
LOG_INFO("UsdSceneRenderer initialized.");
InitGridResources();
InitAxisResources();
InitBBoxResources();
InitCamWireResources();
InitLightWireResources();
}
// ===========================================================================
// Render delegate
// ===========================================================================
/*static*/ std::vector<pxr::TfToken> UsdSceneRenderer::GetRendererPlugins() {
return pxr::UsdImagingGLEngine::GetRendererPlugins();
}
pxr::TfToken UsdSceneRenderer::GetCurrentRendererId() const {
if (m_renderer) return m_renderer->GetCurrentRendererId();
return m_currentRendererPlugin;
}
/*static*/ std::string UsdSceneRenderer::GetRendererDisplayName(const pxr::TfToken& pluginId) {
return pxr::UsdImagingGLEngine::GetRendererDisplayName(pluginId);
}
bool UsdSceneRenderer::SetRendererPlugin(const pxr::TfToken& pluginId) {
m_currentRendererPlugin = pluginId;
if (!m_renderer) {
// Will be picked up on first Render() call via InitRenderer()
return true;
}
bool ok = m_renderer->SetRendererPlugin(pluginId);
if (ok) {
// Reset AOV to "color" on every delegate switch (mirrors stageView._handleRendererChanged)
m_currentAov = pxr::TfToken("color");
m_renderer->SetRendererAov(m_currentAov);
LOG_INFO("Render delegate switched to: " + std::string(pluginId.GetText()));
} else {
LOG_ERROR("Failed to switch render delegate to: " + std::string(pluginId.GetText()));
}
return ok;
}
pxr::TfTokenVector UsdSceneRenderer::GetRendererAovs() const
{
if (!m_renderer) return {};
return m_renderer->GetRendererAovs();
}
bool UsdSceneRenderer::SetCurrentAov(const pxr::TfToken& aov)
{
if (!m_renderer) return false;
if (m_renderer->SetRendererAov(aov)) {
m_currentAov = aov;
return true;
}
return false;
}
pxr::UsdImagingGLRendererSettingsList UsdSceneRenderer::GetRendererSettingsList() const
{
if (!m_renderer) return {};
return m_renderer->GetRendererSettingsList();
}
pxr::VtValue UsdSceneRenderer::GetRendererSetting(const pxr::TfToken& id) const
{
if (!m_renderer) return {};
return m_renderer->GetRendererSetting(id);
}
void UsdSceneRenderer::SetRendererSetting(const pxr::TfToken& id, const pxr::VtValue& v)
{
if (m_renderer) m_renderer->SetRendererSetting(id, v);
}
// ===========================================================================
// Camera State
// ===========================================================================
void UsdSceneRenderer::SetCameraState(
const pxr::GfMatrix4d& viewMatrix,
const pxr::GfMatrix4d& projMatrix)
{
m_viewMatrix = viewMatrix;
m_projMatrix = projMatrix;
m_useCameraPath = false;
m_clipPlanes.clear();
}
void UsdSceneRenderer::SetCameraStateFromGfCamera(const pxr::GfCamera& gfCamera)
{
pxr::GfFrustum frustum = gfCamera.GetFrustum();
m_viewMatrix = frustum.ComputeViewMatrix();
m_projMatrix = frustum.ComputeProjectionMatrix();
m_useCameraPath = false;
m_cameraFrustum = frustum;
m_hasCameraFrustum = true;
// Extract clip planes from GfCamera (same as stageView's renderParams.clipPlanes)
m_clipPlanes.clear();
for (const auto& p : gfCamera.GetClippingPlanes()) {
m_clipPlanes.emplace_back(
static_cast<double>(p[0]), static_cast<double>(p[1]),
static_cast<double>(p[2]), static_cast<double>(p[3]));
}
}
void UsdSceneRenderer::SetCameraPath(const pxr::SdfPath& cameraPath)
{
m_cameraPath = cameraPath;
m_useCameraPath = true;
}
// ===========================================================================
// Selection
// ===========================================================================
void UsdSceneRenderer::ClearSelected()
{
if (m_renderer) m_renderer->ClearSelected();
}
void UsdSceneRenderer::AddSelected(const pxr::SdfPath& path, int instanceIndex)
{
if (m_renderer) m_renderer->AddSelected(path, instanceIndex);
}
void UsdSceneRenderer::SetSelectedPaths(const pxr::SdfPathVector& paths)
{
if (m_renderer) m_renderer->SetSelected(paths);
}
// ===========================================================================
// Bounds
// ===========================================================================
pxr::GfRange3d UsdSceneRenderer::ComputeStageBounds()
{
if (!m_stage) return {};
pxr::TfTokenVector purposes = {
pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
pxr::UsdGeomBBoxCache bboxCache(m_currentTime, purposes, true);
return bboxCache.ComputeWorldBound(m_stage->GetPseudoRoot()).ComputeAlignedRange();
}
// ===========================================================================
// Picking (stageView.pick / computePickFrustum port)
// ===========================================================================
bool UsdSceneRenderer::PickObject(
int mouseX, int mouseY,
int viewWidth, int viewHeight,
pxr::GfVec3d* outHitPoint,
pxr::SdfPath* outHitPrimPath)
{
if (!m_renderer || !m_stage) return false;
if (!m_hasCameraFrustum) return false;
// Normalize mouse to NDC [-1, 1]; Y is flipped (screen Y-down → NDC Y-up)
double nx = (static_cast<double>(mouseX) / static_cast<double>(viewWidth)) * 2.0 - 1.0;
double ny = 1.0 - (static_cast<double>(mouseY) / static_cast<double>(viewHeight)) * 2.0;
pxr::GfVec2d point(nx, ny);
// Pick window: one pixel in NDC
pxr::GfVec2d size(1.0 / static_cast<double>(viewWidth),
1.0 / static_cast<double>(viewHeight));
// Build narrow pick frustum from the stored camera frustum
// (mirrors stageView.computePickFrustum / GfFrustum.ComputeNarrowedFrustum)
pxr::GfFrustum pickFrustum = m_cameraFrustum.ComputeNarrowedFrustum(point, size);
pxr::UsdImagingGLRenderParams pickParams;
pickParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_GEOM_ONLY; // no shading: faster pick pass
pickParams.showGuides = false;
pickParams.showProxy = true;
pickParams.showRender = false;
pickParams.enableSampleAlphaToCoverage = false;
pickParams.enableLighting = false;
pxr::GfVec3d hitPoint, hitNormal;
pxr::SdfPath hitPrimPath;
bool hit = m_renderer->TestIntersection(
pickFrustum.ComputeViewMatrix(),
pickFrustum.ComputeProjectionMatrix(),
m_stage->GetPseudoRoot(),
pickParams,
&hitPoint, &hitNormal, &hitPrimPath);
if (hit) {
if (outHitPoint) *outHitPoint = hitPoint;
if (outHitPrimPath) *outHitPrimPath = hitPrimPath;
LOG_INFO("PickObject hit: " + hitPrimPath.GetString());
}
return hit;
}
bool UsdSceneRenderer::PickObjectsInRect(
int x0, int y0, int x1, int y1,
int viewWidth, int viewHeight,
pxr::SdfPathVector* outHitPaths)
{
if (!m_renderer || !m_stage || !m_hasCameraFrustum) return false;
if (!outHitPaths) return false;
outHitPaths->clear();
// Clamp rect to viewport bounds
x0 = std::max(0, std::min(x0, viewWidth - 1));
x1 = std::max(0, std::min(x1, viewWidth - 1));
y0 = std::max(0, std::min(y0, viewHeight - 1));
y1 = std::max(0, std::min(y1, viewHeight - 1));
if (x0 > x1) std::swap(x0, x1);
if (y0 > y1) std::swap(y0, y1);
if (x0 == x1 || y0 == y1) return false;
// Convert rect to NDC [-1, 1]; Y is flipped (screen Y-down → NDC Y-up)
double ndcCenterX = ((static_cast<double>(x0 + x1) * 0.5) / viewWidth) * 2.0 - 1.0;
double ndcCenterY = 1.0 - ((static_cast<double>(y0 + y1) * 0.5) / viewHeight) * 2.0;
double ndcSizeX = static_cast<double>(x1 - x0) / viewWidth;
double ndcSizeY = static_cast<double>(y1 - y0) / viewHeight;
pxr::GfVec2d center(ndcCenterX, ndcCenterY);
pxr::GfVec2d size (ndcSizeX, ndcSizeY);
// Narrow the stored camera frustum to the selection rect
pxr::GfFrustum pickFrustum = m_cameraFrustum.ComputeNarrowedFrustum(center, size);
pxr::UsdImagingGLRenderParams pickParams;
pickParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_GEOM_ONLY; // no shading: faster pick pass
pickParams.showGuides = false;
pickParams.showProxy = true;
pickParams.showRender = false;
pickParams.enableSampleAlphaToCoverage = false;
pickParams.enableLighting = false;
// resolveUnique: single pick-buffer render pass, returns all unique VISIBLE prims.
// Much faster than resolveDeep (which does a full deep-selection traversal).
pxr::UsdImagingGLEngine::PickParams pp;
pp.resolveMode = pxr::TfToken("resolveUnique");
pxr::UsdImagingGLEngine::IntersectionResultVector results;
bool hit = m_renderer->TestIntersection(
pp,
pickFrustum.ComputeViewMatrix(),
pickFrustum.ComputeProjectionMatrix(),
m_stage->GetPseudoRoot(),
pickParams,
&results);
if (hit) {
// Deduplicate by prim path (a prim may appear multiple times for
// different instances or mesh subsets within the rect).
std::unordered_set<std::string> seen;
for (const auto& r : results) {
const std::string& s = r.hitPrimPath.GetString();
if (!s.empty() && seen.insert(s).second) {
outHitPaths->push_back(r.hitPrimPath);
}
}
LOG_INFO("PickObjectsInRect: " + std::to_string(outHitPaths->size()) + " prims selected");
}
return !outHitPaths->empty();
}
// ===========================================================================
// Render
// ===========================================================================
void UsdSceneRenderer::Render(int width, int height)
{
if (!m_stage || width <= 0 || height <= 0) return;
m_lastRenderWidth = width;
m_lastRenderHeight = height;
InitRenderer();
if (!m_renderer) return;
// (Re)create draw target when it doesn't exist or when the AA/MSAA
// preference has changed. With AA enabled we request a multisampled
// FBO so Hydra and all overlay geometry benefit from hardware MSAA.
const bool wantMSAA = m_aaEnabled;
if (m_drawTarget && m_drawTarget->HasMSAA() != wantMSAA) {
if (m_drawTarget->IsBound()) m_drawTarget->Unbind();
m_drawTarget.Reset();
LOG_INFO("DrawTarget recreated: AA toggled (" +
std::string(wantMSAA ? "MSAA on" : "MSAA off") + ")");
}
if (!m_drawTarget) {
m_drawTarget = pxr::GlfDrawTarget::New(pxr::GfVec2i(width, height), wantMSAA);
if (!m_drawTarget) { LOG_ERROR("Failed to create GlfDrawTarget"); return; }
m_drawTarget->Bind();
// RGBA16F: Hydra's internal render buffers carry linear HDR values;
// RGBA16F on the draw target preserves precision in the corrected output.
m_drawTarget->AddAttachment("color", GL_RGBA, GL_FLOAT, GL_RGBA16F);
m_drawTarget->AddAttachment("depth",
GL_DEPTH_COMPONENT, GL_FLOAT, GL_DEPTH_COMPONENT32F);
m_drawTarget->Unbind();
LOG_INFO("DrawTarget created: fboId="
+ std::to_string(m_drawTarget->GetFramebufferId())
+ " msaa=" + std::string(wantMSAA ? "yes" : "no")
+ " size=" + std::to_string(width) + "x" + std::to_string(height));
}
// Bind first, then resize if needed.
// GlfDrawTarget::SetSize requires the FBO to be bound (asserts otherwise).
m_drawTarget->Bind();
pxr::GfVec2i desiredSize(width, height);
if (m_drawTarget->GetSize() != desiredSize) {
m_drawTarget->SetSize(desiredSize);
}
// Clear
glViewport(0, 0, width, height);
glClearColor(m_backgroundColor[0], m_backgroundColor[1],
m_backgroundColor[2], 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// --- Camera state ---
if (m_useCameraPath && !m_cameraPath.IsEmpty()) {
m_renderer->SetCameraPath(m_cameraPath);
} else {
m_renderer->SetCameraState(m_viewMatrix, m_projMatrix);
}
// --- Use SetRenderBufferSize + SetFraming instead of deprecated SetRenderViewport ---
// (mirrors stageView.paintGL: renderer.SetRenderBufferSize + renderer.SetFraming)
m_renderer->SetRenderBufferSize(pxr::GfVec2i(width, height));
m_renderer->SetFraming(ComputeCameraFraming(0, 0, width, height, width, height));
m_renderer->SetOverrideWindowPolicy(pxr::CameraUtilMatchVertically);
// --- Lighting: mirrors stageView.py paintGL lighting setup ---
// stageView uses two optional lights controlled by viewSettings:
// ambientLightOnly (default True) → camera headlight (point at cam pos)
// domeLightEnabled (default False) → dome/IBL light
// sceneAmbient and material values from viewSettingsDataModel.py defaults.
pxr::GfVec4f sceneAmbient(0.01f, 0.01f, 0.01f, 1.0f);
pxr::GlfSimpleMaterial material;
float kA = m_defaultMaterialAmbient; // 0.2
float kS = m_defaultMaterialSpecular; // 0.1
material.SetAmbient (pxr::GfVec4f(kA, kA, kA, 1.0f));
material.SetSpecular(pxr::GfVec4f(kS, kS, kS, 1.0f));
material.SetShininess(32.0f);
pxr::GlfSimpleLightVector lights;
// Camera headlight: point light (w=1) positioned at the camera world-origin,
// transformed by the view-inverse so it tracks the camera each frame.
// (stageView.py: l.position = cam_pos + (1,); l.transform = frustum.ComputeViewInverse())
//
// The headlight is the *default* fill used only while the stage has no
// authored lights. Once the scene contains real UsdLux lights, suppress it
// so the scene is lit purely by those lights (Hydra evaluates them via
// enableSceneLights) -- mirrors usdview's "use scene lights when present".
const bool stageHasLights = StageHasAuthoredLights();
if (m_ambientLightOnly && !stageHasLights) {
pxr::GfMatrix4d viewInverse = m_viewMatrix.GetInverse();
pxr::GfVec3d camPos = viewInverse.ExtractTranslation();
pxr::GlfSimpleLight camLight;
camLight.SetAmbient (pxr::GfVec4f(0.0f, 0.0f, 0.0f, 0.0f));
camLight.SetDiffuse (pxr::GfVec4f(1.0f, 1.0f, 1.0f, 1.0f));
camLight.SetSpecular(pxr::GfVec4f(1.0f, 1.0f, 1.0f, 1.0f));
camLight.SetPosition(pxr::GfVec4f(
static_cast<float>(camPos[0]),
static_cast<float>(camPos[1]),
static_cast<float>(camPos[2]),
1.0f)); // w=1 → point light
camLight.SetTransform(viewInverse);
lights.push_back(camLight);
}
// Dome light (IBL): isDomeLight=true, Z-up stages need a 90° X-axis rotation.
// (stageView.py: l.isDomeLight = True; if stageIsZup: l.transform = rot90X)
if (m_domeLightEnabled) {
pxr::GlfSimpleLight domeLight;
domeLight.SetIsDomeLight(true);
if (m_stageIsZup) {
pxr::GfMatrix4d rot;
rot.SetRotate(pxr::GfRotation(pxr::GfVec3d::XAxis(), 90.0));
domeLight.SetTransform(rot);
}
lights.push_back(domeLight);
}
m_renderer->SetLightingState(lights, material, sceneAmbient);
// --- Render params (matches stageView.renderSinglePass) ---
m_renderParams = pxr::UsdImagingGLRenderParams();
m_renderParams.frame = m_currentTime;
m_renderParams.complexity = m_complexity;
// Apply shading mode → drawMode + enableLighting
switch (m_shadingMode) {
case ShadingMode::FlatShaded:
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_SHADED_FLAT;
m_renderParams.enableLighting = true; break;
case ShadingMode::WireframeOnSurface:
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_WIREFRAME_ON_SURFACE;
m_renderParams.enableLighting = true; break;
case ShadingMode::Wireframe:
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_WIREFRAME;
m_renderParams.enableLighting = false; break;
case ShadingMode::Unlit:
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_SHADED_SMOOTH;
m_renderParams.enableLighting = false; break;
default: // SmoothShaded
m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_SHADED_SMOOTH;
m_renderParams.enableLighting = true; break;
}
m_renderParams.showGuides = m_showGuides;
m_renderParams.showProxy = m_showProxy;
m_renderParams.showRender = m_showRender;
m_renderParams.enableSampleAlphaToCoverage = true;
m_renderParams.gammaCorrectColors = false;
m_renderParams.cullStyle = static_cast<pxr::UsdImagingGLCullStyle>(m_cullStyle);
m_renderParams.enableSceneMaterials = true;
m_renderParams.enableSceneLights = true;
m_renderParams.highlight = true;
m_renderParams.clearColor = pxr::GfVec4f(
m_backgroundColor[0], m_backgroundColor[1], m_backgroundColor[2], 1.0f);
m_renderParams.forceRefresh = m_forceRefresh;
m_renderParams.clipPlanes = m_clipPlanes;
// Color correction — stageView.py approach: pass the actual mode + OCIO
// params to HdxColorCorrectionTask via RenderParams and SetColorCorrection-
// Settings (mirrors stageView.renderSinglePass).
pxr::TfToken ccToken("disabled");
if (m_colorCorrectionMode == ColorCorrectionMode::sRGB)
ccToken = pxr::TfToken("sRGB");
else if (m_colorCorrectionMode == ColorCorrectionMode::OpenColorIO)
ccToken = pxr::TfToken("openColorIO");
m_renderParams.colorCorrectionMode = ccToken;
if (m_colorCorrectionMode == ColorCorrectionMode::OpenColorIO) {
m_renderParams.ocioDisplay = pxr::TfToken(m_ocioDisplay);
m_renderParams.ocioView = pxr::TfToken(m_ocioView);
m_renderParams.ocioColorSpace = pxr::TfToken(m_ocioColorSpace);
m_renderParams.ocioLook = pxr::TfToken(m_ocioLook);
}
m_renderer->SetColorCorrectionSettings(
ccToken,
pxr::TfToken(m_ocioDisplay),
pxr::TfToken(m_ocioView),
pxr::TfToken(m_ocioColorSpace),
pxr::TfToken(m_ocioLook));
// Guard the Hydra render: if it throws, the m_drawTarget->Unbind() below
// would be skipped, permanently unbalancing the GlfDrawTarget bind stack
// and blacking out every later frame.
try {
m_renderer->Render(m_stage->GetPseudoRoot(), m_renderParams);
} catch (const std::exception& e) {
LOG_ERROR("Hydra render failed: " + std::string(e.what()));
}
m_forceRefresh = false;
// --- Optional grid overlay ---
if (m_showGrid) {
// When MSAA is active, render the grid into the MSAA FBO so it is
// also multisampled and resolved together with the Hydra output.
GLuint gridFbo = m_drawTarget->HasMSAA()
? m_drawTarget->GetFramebufferMSId()
: m_drawTarget->GetFramebufferId();
glBindFramebuffer(GL_FRAMEBUFFER, gridFbo);
glViewport(0, 0, width, height);
RenderGrid(width, height);
}
// --- Diagnostic logging ---
if (m_diagFrameCount < 3) {
auto att = m_drawTarget->GetAttachment("color");
if (att) {
LOG_INFO("Frame " + std::to_string(m_diagFrameCount)
+ ": texId=" + std::to_string(att->GetGlTextureName())
+ " fboId=" + std::to_string(m_drawTarget->GetFramebufferId())
+ " size=" + std::to_string(width) + "x" + std::to_string(height)
+ " bg=" + Vec3fStr(m_backgroundColor));
}
++m_diagFrameCount;
}
m_drawTarget->Unbind();
}
// ===========================================================================
// Output
// ===========================================================================
uint32_t UsdSceneRenderer::GetColorTextureID()
{
if (!m_drawTarget) return 0;
// Resolve MSAA → regular texture before the caller samples it.
// Called after all overlay draws (axis, bboxes, camera wireframes) so
// every layer of MSAA-rendered content is included in the resolve.
m_drawTarget->Resolve();
auto att = m_drawTarget->GetAttachment("color");
return att ? static_cast<uint32_t>(att->GetGlTextureName()) : 0;
}
bool UsdSceneRenderer::CaptureFrame(std::vector<uint8_t>& outRGBA)
{
if (!m_drawTarget || m_lastRenderWidth <= 0 || m_lastRenderHeight <= 0)
return false;
int w = m_lastRenderWidth;
int h = m_lastRenderHeight;
// Resolve MSAA so we read from the non-multisampled colour attachment.
m_drawTarget->Resolve();
outRGBA.resize(static_cast<size_t>(w) * h * 4);
// Save/restore the read-framebuffer binding so we don't upset ImGui.
GLint prevFbo = 0;
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevFbo);
glBindFramebuffer(GL_READ_FRAMEBUFFER, m_drawTarget->GetFramebufferId());
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, outRGBA.data());
glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(prevFbo));
// OpenGL reads bottom-up; flip to top-down.
int stride = w * 4;
std::vector<uint8_t> row(stride);
for (int y = 0; y < h / 2; ++y) {
uint8_t* top = outRGBA.data() + y * stride;
uint8_t* bot = outRGBA.data() + (h - 1 - y) * stride;
std::copy(top, top + stride, row.data());
std::copy(bot, bot + stride, top);
std::copy(row.data(), row.data() + stride, bot);
}
return true;
}
// ===========================================================================
// Axis Overlay (stageView.DrawAxis port)
// ===========================================================================
void UsdSceneRenderer::InitAxisResources()
{
if (m_axisProgram != 0) return;
GLuint vs = CompileShader(kAxisVS, GL_VERTEX_SHADER);
GLuint fs = CompileShader(kAxisFS, GL_FRAGMENT_SHADER);
if (!vs || !fs) { glDeleteShader(vs); glDeleteShader(fs); return; }
m_axisProgram = LinkProgram(vs, fs);
if (!m_axisProgram) return;
m_axisUniformMVP = glGetUniformLocation(m_axisProgram, "mvpMatrix");
m_axisUniformColor = glGetUniformLocation(m_axisProgram, "color");
// 3 line segments: X=(1,0,0), Y=(0,1,0), Z=(0,0,1) from origin (0,0,0)
float axisVerts[] = {
1,0,0, 0,0,0,
0,1,0, 0,0,0,
0,0,1, 0,0,0
};
glGenVertexArrays(1, &m_axisVAO);
glGenBuffers(1, &m_axisVBO);
glBindVertexArray(m_axisVAO);
glBindBuffer(GL_ARRAY_BUFFER, m_axisVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(axisVerts), axisVerts, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
glBindVertexArray(0);
LOG_INFO("Axis resources initialized.");
}
void UsdSceneRenderer::DestroyAxisResources()
{
if (m_axisVAO) { glDeleteVertexArrays(1, &m_axisVAO); m_axisVAO = 0; }
if (m_axisVBO) { glDeleteBuffers(1, &m_axisVBO); m_axisVBO = 0; }
if (m_axisProgram) { glDeleteProgram(m_axisProgram); m_axisProgram = 0; }
}
void UsdSceneRenderer::DrawAxis(
const pxr::GfMatrix4d& viewProjMatrix, double cameraDist)
{
if (!m_axisProgram || !m_axisVAO) return;
if (!m_drawTarget) return;
// Overlays are called after Render() has unbound the FBO.
// Re-bind so we draw into the offscreen texture, not the default framebuffer.
pxr::GfVec2i sz = m_drawTarget->GetSize();
m_drawTarget->Bind();
glViewport(0, 0, sz[0], sz[1]);
// Scale the gizmo to stay roughly fixed in screen space (stageView: dist/20)
pxr::GfMatrix4f mvp =
pxr::GfMatrix4f(1.0f).SetScale(static_cast<float>(cameraDist / 20.0))
* pxr::GfMatrix4f(viewProjMatrix);
glUseProgram(m_axisProgram);
glBindVertexArray(m_axisVAO);
glUniformMatrix4fv(m_axisUniformMVP, 1, GL_TRUE, mvp.GetArray());
GLboolean prevDepthMask;
glGetBooleanv(GL_DEPTH_WRITEMASK, &prevDepthMask);
glDepthMask(GL_FALSE);
GLboolean prevDepthTest = glIsEnabled(GL_DEPTH_TEST);
GLboolean prevLineSmooth = glIsEnabled(GL_LINE_SMOOTH);
glEnable(GL_DEPTH_TEST);
if (m_aaEnabled) { glEnable(GL_LINE_SMOOTH); glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); }
// X axis: red
glUniform4f(m_axisUniformColor, 1, 0, 0, 1);
glDrawArrays(GL_LINES, 0, 2);
// Y axis: green
glUniform4f(m_axisUniformColor, 0, 1, 0, 1);
glDrawArrays(GL_LINES, 2, 2);
// Z axis: blue
glUniform4f(m_axisUniformColor, 0, 0, 1, 1);
glDrawArrays(GL_LINES, 4, 2);
glDepthMask(prevDepthMask);
if (!prevDepthTest) glDisable(GL_DEPTH_TEST);
if (prevLineSmooth) glEnable(GL_LINE_SMOOTH);
else glDisable(GL_LINE_SMOOTH);
glBindVertexArray(0);
glUseProgram(0);
m_drawTarget->Unbind();
}
// ===========================================================================
// Grid Overlay
// ===========================================================================
void UsdSceneRenderer::InitGridResources()
{
if (m_gridVAO != 0) return;
// Grid reuses the axis shader program — both use vec3 position + uniform MVP/color.
// The VAO only needs a position attribute.
glGenVertexArrays(1, &m_gridVAO);
glGenBuffers(1, &m_gridVBO);
glBindVertexArray(m_gridVAO);
glBindBuffer(GL_ARRAY_BUFFER, m_gridVBO);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr);
glBindVertexArray(0);
LOG_INFO("Grid VAO/VBO created.");
RebuildGridVBO();
}
void UsdSceneRenderer::RebuildGridVBO()
{
// Can only rebuild once the VAO/VBO have been created.
if (m_gridVAO == 0 || m_gridVBO == 0) return;
// -------------------------------------------------------------------------
// Build all grid line vertices grouped by category so we can draw each
// group with a different colour in a single draw call.
//
// Group A — minor lines (1-unit spacing, excluding multiples of 10 and 0)
// Group B — major lines (10-unit spacing, excluding 0)
// Group C — "A"-axis (the axis along direction a, at b = 0 → red )
// Group D — "B"-axis (the axis along direction b, at a = 0 → blue/green)
//
// For Y-up: ground plane = XZ (Y=0), a = X axis, b = Z axis.
// For Z-up: ground plane = XY (Z=0), a = X axis, b = Y axis.
// -------------------------------------------------------------------------
const float H = m_gridHalfSize; // half-extent (default 50)
const int N = static_cast<int>(H); // integer half-extent (e.g. 50)
const int maj = 10; // major-line interval
// Helper: given a 2-D coordinate pair (a, b) on the ground plane,
// return the 3-D world position based on the current up-axis.
auto toWorld = [&](float a, float b) -> std::array<float,3> {
if (m_stageIsZup)
return { a, b, 0.0f }; // XY plane at Z=0
else
return { a, 0.0f, b }; // XZ plane at Y=0
};
std::vector<float> minor_verts, major_verts, axisA_verts, axisB_verts;
// Lines parallel to the A-axis (at fixed b values):
for (int bi = -N; bi <= N; ++bi) {
float b = static_cast<float>(bi);
auto p0 = toWorld(-H, b);
auto p1 = toWorld( H, b);
if (bi == 0) {
// A-axis (red)
axisA_verts.insert(axisA_verts.end(), p0.begin(), p0.end());
axisA_verts.insert(axisA_verts.end(), p1.begin(), p1.end());
} else if (bi % maj == 0) {
// Major line
major_verts.insert(major_verts.end(), p0.begin(), p0.end());
major_verts.insert(major_verts.end(), p1.begin(), p1.end());
} else {
// Minor line
minor_verts.insert(minor_verts.end(), p0.begin(), p0.end());
minor_verts.insert(minor_verts.end(), p1.begin(), p1.end());
}
}
// Lines parallel to the B-axis (at fixed a values):
for (int ai = -N; ai <= N; ++ai) {
float a = static_cast<float>(ai);
auto p0 = toWorld(a, -H);
auto p1 = toWorld(a, H);
if (ai == 0) {
// B-axis (blue / green)
axisB_verts.insert(axisB_verts.end(), p0.begin(), p0.end());
axisB_verts.insert(axisB_verts.end(), p1.begin(), p1.end());
} else if (ai % maj == 0) {
major_verts.insert(major_verts.end(), p0.begin(), p0.end());
major_verts.insert(major_verts.end(), p1.begin(), p1.end());
} else {
minor_verts.insert(minor_verts.end(), p0.begin(), p0.end());
minor_verts.insert(minor_verts.end(), p1.begin(), p1.end());
}
}
// Pack into one contiguous buffer: [minor | major | axisA | axisB]
std::vector<float> all;
all.reserve(minor_verts.size() + major_verts.size() +
axisA_verts.size() + axisB_verts.size());
auto floatsToVerts = [](size_t f) { return static_cast<GLint>(f / 3); };
m_gridMinorFirst = 0;
m_gridMinorCount = floatsToVerts(minor_verts.size());
all.insert(all.end(), minor_verts.begin(), minor_verts.end());
m_gridMajorFirst = m_gridMinorFirst + m_gridMinorCount;
m_gridMajorCount = floatsToVerts(major_verts.size());
all.insert(all.end(), major_verts.begin(), major_verts.end());
m_gridAxisAFirst = m_gridMajorFirst + m_gridMajorCount;
m_gridAxisACount = floatsToVerts(axisA_verts.size());
all.insert(all.end(), axisA_verts.begin(), axisA_verts.end());
m_gridAxisBFirst = m_gridAxisAFirst + m_gridAxisACount;
m_gridAxisBCount = floatsToVerts(axisB_verts.size());
all.insert(all.end(), axisB_verts.begin(), axisB_verts.end());
glBindBuffer(GL_ARRAY_BUFFER, m_gridVBO);
glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(all.size() * sizeof(float)),
all.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
LOG_INFO("Grid VBO rebuilt: "
+ std::to_string(m_gridMinorCount / 2) + " minor lines, "
+ std::to_string(m_gridMajorCount / 2) + " major lines, "
+ std::string(m_stageIsZup ? "Z-up" : "Y-up"));
}
void UsdSceneRenderer::DestroyGridResources()
{
if (m_gridVAO) { glDeleteVertexArrays(1, &m_gridVAO); m_gridVAO = 0; }
if (m_gridVBO) { glDeleteBuffers(1, &m_gridVBO); m_gridVBO = 0; }
// No separate grid program — the axis program is destroyed in DestroyAxisResources().
}
void UsdSceneRenderer::RenderGrid(int /*width*/, int /*height*/)
{
if (!m_gridVAO || !m_axisProgram) return;
// Grid reuses the axis shader.
// MVP = view * proj (no extra scale — grid is already in world units).
pxr::GfMatrix4f mvp(m_viewMatrix * m_projMatrix);
// Helper: enable/disable GL_LINE_SMOOTH depending on the AA setting.
auto setLineAA = [&](bool enable) {
if (enable) {
glEnable(GL_LINE_SMOOTH);
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
} else {
glDisable(GL_LINE_SMOOTH);
}
};
// Save relevant GL state.
GLboolean prevDepthMask;
GLboolean prevDepthTest = glIsEnabled(GL_DEPTH_TEST);
GLboolean prevBlend = glIsEnabled(GL_BLEND);
GLboolean prevLineSmooth= glIsEnabled(GL_LINE_SMOOTH);
GLint prevDepthFunc, prevBlendSrc, prevBlendDst;
glGetBooleanv(GL_DEPTH_WRITEMASK, &prevDepthMask);
glGetIntegerv(GL_DEPTH_FUNC, &prevDepthFunc);
glGetIntegerv(GL_BLEND_SRC_ALPHA, &prevBlendSrc);
glGetIntegerv(GL_BLEND_DST_ALPHA, &prevBlendDst);
glDepthMask(GL_FALSE);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
setLineAA(m_aaEnabled);
glUseProgram(m_axisProgram);
glUniformMatrix4fv(m_axisUniformMVP, 1, GL_TRUE, mvp.GetArray());
glBindVertexArray(m_gridVAO);
// --- Minor lines — dark grey ---
glLineWidth(1.0f);
glUniform4f(m_axisUniformColor, 0.35f, 0.35f, 0.35f, 1.0f);
if (m_gridMinorCount > 0)
glDrawArrays(GL_LINES, m_gridMinorFirst, m_gridMinorCount);
// --- Major lines — medium grey ---
glUniform4f(m_axisUniformColor, 0.52f, 0.52f, 0.52f, 1.0f);
if (m_gridMajorCount > 0)
glDrawArrays(GL_LINES, m_gridMajorFirst, m_gridMajorCount);
// --- A-axis (at b=0): X axis — red ---
glLineWidth(m_aaEnabled ? 1.5f : 1.0f);
glUniform4f(m_axisUniformColor, 0.62f, 0.28f, 0.28f, 1.0f);
if (m_gridAxisACount > 0)
glDrawArrays(GL_LINES, m_gridAxisAFirst, m_gridAxisACount);
// --- B-axis (at a=0): Z-axis (blue) for Y-up, Y-axis (green) for Z-up ---
if (m_stageIsZup)
glUniform4f(m_axisUniformColor, 0.28f, 0.55f, 0.28f, 1.0f); // green (Y)
else
glUniform4f(m_axisUniformColor, 0.28f, 0.28f, 0.62f, 1.0f); // blue (Z)
if (m_gridAxisBCount > 0)
glDrawArrays(GL_LINES, m_gridAxisBFirst, m_gridAxisBCount);
glBindVertexArray(0);
glUseProgram(0);
// Restore GL state.
glLineWidth(1.0f);
glDepthMask(prevDepthMask);
glDepthFunc(static_cast<GLenum>(prevDepthFunc));
glBlendFunc(static_cast<GLenum>(prevBlendSrc),
static_cast<GLenum>(prevBlendDst));
if (!prevDepthTest) glDisable(GL_DEPTH_TEST);
if (!prevBlend) glDisable(GL_BLEND);
if (prevLineSmooth) glEnable(GL_LINE_SMOOTH);
else glDisable(GL_LINE_SMOOTH);
}
// ===========================================================================
// Bounding Box Overlay
// ===========================================================================
// Reuse the same simple line shader as the axis gizmo.
static const char* kBBoxVS = R"(#version 130
in vec3 position;
uniform mat4 mvpMatrix;
void main() { gl_Position = vec4(position, 1.0) * mvpMatrix; }
)";
static const char* kBBoxFS = R"(#version 130
uniform vec4 color;
out vec4 outColor;
void main() { outColor = color; }
)";
void UsdSceneRenderer::InitBBoxResources()
{
if (m_bboxProgram != 0) return;
GLuint vs = CompileShader(kBBoxVS, GL_VERTEX_SHADER);
GLuint fs = CompileShader(kBBoxFS, GL_FRAGMENT_SHADER);
if (!vs || !fs) { glDeleteShader(vs); glDeleteShader(fs); return; }
m_bboxProgram = LinkProgram(vs, fs);
if (!m_bboxProgram) return;
m_bboxUniformMVP = glGetUniformLocation(m_bboxProgram, "mvpMatrix");
m_bboxUniformColor = glGetUniformLocation(m_bboxProgram, "color");
// Allocate a VAO/VBO for 24 vertices (12 edges × 2 endpoints) — data uploaded dynamically.
glGenVertexArrays(1, &m_bboxVAO);
glGenBuffers(1, &m_bboxVBO);
glBindVertexArray(m_bboxVAO);
glBindBuffer(GL_ARRAY_BUFFER, m_bboxVBO);
glBufferData(GL_ARRAY_BUFFER, 24 * 3 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
glBindVertexArray(0);
LOG_INFO("BBox resources initialized.");
}
void UsdSceneRenderer::DestroyBBoxResources()
{
if (m_bboxVAO) { glDeleteVertexArrays(1, &m_bboxVAO); m_bboxVAO = 0; }
if (m_bboxVBO) { glDeleteBuffers(1, &m_bboxVBO); m_bboxVBO = 0; }
if (m_bboxProgram) { glDeleteProgram(m_bboxProgram); m_bboxProgram = 0; }
}
// ──────────────────────────────────────────────────────────────────────────────
// Camera Wireframe GL resources
// Uses the same simple line shader as the axis / bbox overlays.
// A separate VAO/VBO avoids any interaction with the bbox dynamic VBO.
// ──────────────────────────────────────────────────────────────────────────────
void UsdSceneRenderer::InitCamWireResources()
{
if (m_camWireVAO != 0) return;
if (!m_bboxProgram) return; // reuse the already-compiled bbox shader program
glGenVertexArrays(1, &m_camWireVAO);
glGenBuffers(1, &m_camWireVBO);
glBindVertexArray(m_camWireVAO);
glBindBuffer(GL_ARRAY_BUFFER, m_camWireVBO);
// Allocate an initial budget; DrawCameraWireframes reallocates per frame.
glBufferData(GL_ARRAY_BUFFER, 256 * 3 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
glBindVertexArray(0);
LOG_INFO("Camera wireframe GL resources initialized.");
}
void UsdSceneRenderer::DestroyCamWireResources()
{
if (m_camWireVAO) { glDeleteVertexArrays(1, &m_camWireVAO); m_camWireVAO = 0; }
if (m_camWireVBO) { glDeleteBuffers(1, &m_camWireVBO); m_camWireVBO = 0; }
}
void UsdSceneRenderer::InitLightWireResources()
{
if (m_lightWireVAO != 0) return;
if (!m_bboxProgram) return; // reuse the already-compiled bbox shader program
glGenVertexArrays(1, &m_lightWireVAO);
glGenBuffers(1, &m_lightWireVBO);
glBindVertexArray(m_lightWireVAO);
glBindBuffer(GL_ARRAY_BUFFER, m_lightWireVBO);
// Sphere/cylinder gizmos use several circles; budget more than the camera VBO.
glBufferData(GL_ARRAY_BUFFER, 1024 * 3 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
glBindVertexArray(0);
LOG_INFO("Light wireframe GL resources initialized.");
}
void UsdSceneRenderer::DestroyLightWireResources()
{
if (m_lightWireVAO) { glDeleteVertexArrays(1, &m_lightWireVAO); m_lightWireVAO = 0; }
if (m_lightWireVBO) { glDeleteBuffers(1, &m_lightWireVBO); m_lightWireVBO = 0; }
}
void UsdSceneRenderer::DrawBox(
const pxr::GfRange3d& range,
const pxr::GfMatrix4f& mvp)
{
if (range.IsEmpty()) return;
pxr::GfVec3d mn = range.GetMin();
pxr::GfVec3d mx = range.GetMax();
// 8 corners of the AABB
float verts[24][3] = {
// Bottom face
{ (float)mn[0], (float)mn[1], (float)mn[2] }, { (float)mx[0], (float)mn[1], (float)mn[2] },
{ (float)mx[0], (float)mn[1], (float)mn[2] }, { (float)mx[0], (float)mn[1], (float)mx[2] },
{ (float)mx[0], (float)mn[1], (float)mx[2] }, { (float)mn[0], (float)mn[1], (float)mx[2] },
{ (float)mn[0], (float)mn[1], (float)mx[2] }, { (float)mn[0], (float)mn[1], (float)mn[2] },
// Top face
{ (float)mn[0], (float)mx[1], (float)mn[2] }, { (float)mx[0], (float)mx[1], (float)mn[2] },
{ (float)mx[0], (float)mx[1], (float)mn[2] }, { (float)mx[0], (float)mx[1], (float)mx[2] },
{ (float)mx[0], (float)mx[1], (float)mx[2] }, { (float)mn[0], (float)mx[1], (float)mx[2] },
{ (float)mn[0], (float)mx[1], (float)mx[2] }, { (float)mn[0], (float)mx[1], (float)mn[2] },
// Vertical pillars
{ (float)mn[0], (float)mn[1], (float)mn[2] }, { (float)mn[0], (float)mx[1], (float)mn[2] },
{ (float)mx[0], (float)mn[1], (float)mn[2] }, { (float)mx[0], (float)mx[1], (float)mn[2] },
{ (float)mx[0], (float)mn[1], (float)mx[2] }, { (float)mx[0], (float)mx[1], (float)mx[2] },
{ (float)mn[0], (float)mn[1], (float)mx[2] }, { (float)mn[0], (float)mx[1], (float)mx[2] },
};
glBindBuffer(GL_ARRAY_BUFFER, m_bboxVBO);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verts), verts);
glUniformMatrix4fv(m_bboxUniformMVP, 1, GL_TRUE, mvp.GetArray());
glBindVertexArray(m_bboxVAO);
glDrawArrays(GL_LINES, 0, 24);
glBindVertexArray(0);
}
void UsdSceneRenderer::DrawBoundingBoxes(
const pxr::SdfPathVector& selectedPaths,
const pxr::GfMatrix4d& viewProjMatrix)
{
if (m_bboxMode == BBoxMode::None) return;
if (!m_bboxProgram || !m_bboxVAO) return;
if (!m_stage || selectedPaths.empty()) return;
if (!m_drawTarget) return;
// Re-bind the offscreen FBO so we draw into the texture, not the default framebuffer.
pxr::GfVec2i sz = m_drawTarget->GetSize();
m_drawTarget->Bind();
glViewport(0, 0, sz[0], sz[1]);
// Compute world bboxes via UsdGeomBBoxCache
pxr::TfTokenVector purposes = {
pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy };
pxr::UsdGeomBBoxCache bboxCache(
m_currentTime, purposes, /*useExtentsHint=*/true);
// Save/restore GL state
GLboolean prevDepthMask;
glGetBooleanv(GL_DEPTH_WRITEMASK, &prevDepthMask);
GLboolean prevDepthTest = glIsEnabled(GL_DEPTH_TEST);
GLboolean prevBlend = glIsEnabled(GL_BLEND);
glDepthMask(GL_FALSE);
glEnable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glUseProgram(m_bboxProgram);
glUniform4f(m_bboxUniformColor,
m_bboxColor[0], m_bboxColor[1], m_bboxColor[2], m_bboxColor[3]);
pxr::GfMatrix4f vp = pxr::GfMatrix4f(viewProjMatrix);
if (m_bboxMode == BBoxMode::PerObject) {
for (const auto& path : selectedPaths) {
pxr::UsdPrim prim = m_stage->GetPrimAtPath(path);
if (!prim) continue;
pxr::GfBBox3d bbox = bboxCache.ComputeWorldBound(prim);
DrawBox(bbox.ComputeAlignedRange(), vp);
}
} else {
// AllSelection: one combined AABB around all selected prims
pxr::GfRange3d combined;
for (const auto& path : selectedPaths) {
pxr::UsdPrim prim = m_stage->GetPrimAtPath(path);
if (!prim) continue;
pxr::GfBBox3d bbox = bboxCache.ComputeWorldBound(prim);
combined.UnionWith(bbox.ComputeAlignedRange());
}
DrawBox(combined, vp);
}
glDepthMask(prevDepthMask);
if (!prevDepthTest) glDisable(GL_DEPTH_TEST);
if (!prevBlend) glDisable(GL_BLEND);
glUseProgram(0);
m_drawTarget->Unbind();
}
// ===========================================================================
// Draw-target FBO helpers
// ===========================================================================
void UsdSceneRenderer::BindDrawTarget()
{
if (!m_drawTarget) return;
pxr::GfVec2i sz = m_drawTarget->GetSize();
m_drawTarget->Bind();
glViewport(0, 0, sz[0], sz[1]);
}
void UsdSceneRenderer::UnbindDrawTarget()
{
if (m_drawTarget) m_drawTarget->Unbind();
}
// ===========================================================================
// Camera Wireframe helpers
// ===========================================================================
// Project a world-space point to absolute screen coords (imagePosX/Y + pixel offsets).
// Returns false if the point is behind the near plane (w ≤ 0).
bool UsdSceneRenderer::WorldToScreen(const pxr::GfVec3d& world,
const pxr::GfMatrix4d& vp,
int viewW, int viewH,
float imagePosX, float imagePosY,
float& outX, float& outY)
{
double cx = vp[0][0]*world[0] + vp[1][0]*world[1] + vp[2][0]*world[2] + vp[3][0];
double cy = vp[0][1]*world[0] + vp[1][1]*world[1] + vp[2][1]*world[2] + vp[3][1];
double cw = vp[0][3]*world[0] + vp[1][3]*world[1] + vp[2][3]*world[2] + vp[3][3];
if (cw <= 0.0) return false;
double invW = 1.0 / cw;
outX = imagePosX + static_cast<float>(( cx * invW + 1.0) * 0.5 * viewW);
outY = imagePosY + static_cast<float>((1.0 - cy * invW) * 0.5 * viewH);
return true;
}
float UsdSceneRenderer::PointToSegmentDist(float px, float py,
float ax, float ay,
float bx, float by)
{
float dx = bx - ax, dy = by - ay;
float lenSq = dx*dx + dy*dy;
if (lenSq < 1e-6f) {
float ex = px - ax, ey = py - ay;
return std::sqrt(ex*ex + ey*ey);
}
float t = std::max(0.f, std::min(1.f, ((px-ax)*dx + (py-ay)*dy) / lenSq));
float cx2 = ax + t*dx - px;
float cy2 = ay + t*dy - py;
return std::sqrt(cx2*cx2 + cy2*cy2);
}
// Build camera wireframe line segments in world space.
// Geometry: body box (12 edges) + frustum pyramid (4 lines to near quad) + up arrow (1 line).
void UsdSceneRenderer::BuildCameraWireframeLines(const pxr::GfCamera& gfCam,
double scale,
std::vector<float>& outVerts)
{
// Camera transform: rows are camera axes in world space.
// Column convention: cameraToWorld = camMat (USD row-vector).
pxr::GfMatrix4d camToWorld = gfCam.GetTransform();
// Camera origin in world space
pxr::GfVec3d origin(camToWorld[3][0], camToWorld[3][1], camToWorld[3][2]);
// Camera axes (columns of the rotation part, row-major: row i = axis i of camera)
// In USD GfMatrix4d row-vector convention: world = local * M
// So camera right = row 0, up = row 1, -forward = row 2
pxr::GfVec3d right( camToWorld[0][0], camToWorld[0][1], camToWorld[0][2]);
pxr::GfVec3d up( camToWorld[1][0], camToWorld[1][1], camToWorld[1][2]);
pxr::GfVec3d forward(camToWorld[2][0], camToWorld[2][1], camToWorld[2][2]);
// Note: USD cameras look down -Z in local space, so forward here is the +Z local = backward in view.
// The camera shoots along -forward (local -Z).
pxr::GfVec3d lookDir = -forward; // world-space look direction
// ---- Body box ----
double bh = scale * 0.10; // half-size
// 8 corners of the body box in world space
pxr::GfVec3d c[8];
for (int xi = -1; xi <= 1; xi += 2)
for (int yi = -1; yi <= 1; yi += 2)
for (int zi = -1; zi <= 1; zi += 2) {
int idx = ((xi+1)/2) | (((yi+1)/2) << 1) | (((zi+1)/2) << 2);
c[idx] = origin + right*bh*xi + up*bh*yi + lookDir*bh*zi;
}
// 12 edges of the box: connect corners whose indices differ by exactly one bit
static const int kEdges[12][2] = {
{0,1},{2,3},{4,5},{6,7}, // X edges
{0,2},{1,3},{4,6},{5,7}, // Y edges
{0,4},{1,5},{2,6},{3,7} // Z edges
};
auto push = [&](const pxr::GfVec3d& a, const pxr::GfVec3d& b) {
outVerts.push_back(static_cast<float>(a[0]));
outVerts.push_back(static_cast<float>(a[1]));
outVerts.push_back(static_cast<float>(a[2]));
outVerts.push_back(static_cast<float>(b[0]));
outVerts.push_back(static_cast<float>(b[1]));
outVerts.push_back(static_cast<float>(b[2]));
};
for (auto& e : kEdges) push(c[e[0]], c[e[1]]);
// ---- Frustum pyramid (4 lines from origin to near quad corners) ----
// Use perspective projection: half-widths at near depth proportional to aperture/focalLen
double nearDepth = std::max(gfCam.GetClippingRange().GetMin(), 0.01f);
// Cap display depth at scale so the pyramid isn't enormous
double dispDepth = std::min(nearDepth * 3.0, scale * 1.5);
dispDepth = std::max(dispDepth, scale * 0.3);
double hApert = static_cast<double>(gfCam.GetHorizontalAperture()) * 0.5;
double vApert = static_cast<double>(gfCam.GetVerticalAperture()) * 0.5;
double focalL = static_cast<double>(gfCam.GetFocalLength());
if (focalL < 1e-6) focalL = 50.0; // fallback
// Scale aperture to the display depth using similar triangles
double hw = hApert / focalL * dispDepth;
double hv = vApert / focalL * dispDepth;
// 4 corners of the near quad at dispDepth along lookDir
pxr::GfVec3d nearCentre = origin + lookDir * dispDepth;
pxr::GfVec3d nBL = nearCentre - right*hw - up*hv;
pxr::GfVec3d nBR = nearCentre + right*hw - up*hv;
pxr::GfVec3d nTL = nearCentre - right*hw + up*hv;
pxr::GfVec3d nTR = nearCentre + right*hw + up*hv;
// Lines from origin to each corner (pyramid edges)
push(origin, nBL);
push(origin, nBR);
push(origin, nTL);
push(origin, nTR);
// Near quad rectangle
push(nBL, nBR);
push(nBR, nTR);
push(nTR, nTL);
push(nTL, nBL);
// ---- Up arrow ----
pxr::GfVec3d arrowBase = origin + up * bh;
pxr::GfVec3d arrowTip = origin + up * (bh + scale * 0.18);
push(arrowBase, arrowTip);
}
// Builds near quad + far quad + 4 connecting edges for the full GfFrustum.
// Mirrors stageView.py DrawCameraGuides: corners via GfFrustum::ComputeCorners(),
// line pairs via the same 24-index layout. Appends 24 XYZ float triples.
static void BuildCameraGuideLines(const pxr::GfCamera& gfCam,
std::vector<float>& outVerts)
{
const std::vector<pxr::GfVec3d> c = gfCam.GetFrustum().ComputeCorners();
// 0=LBN 1=RBN 2=LTN 3=RTN 4=LBF 5=RBF 6=LTF 7=RTF
static const int kIdx[24] = {
0,1, 1,3, 3,2, 2,0, // near quad
4,5, 5,7, 7,6, 6,4, // far quad
3,7, 0,4, 1,5, 2,6 // connecting edges
};
for (int i : kIdx) {
outVerts.push_back(static_cast<float>(c[i][0]));
outVerts.push_back(static_cast<float>(c[i][1]));
outVerts.push_back(static_cast<float>(c[i][2]));
}
}
// ===========================================================================
// DrawCameraWireframes
// ===========================================================================
void UsdSceneRenderer::DrawCameraWireframes(
pxr::UsdStageRefPtr stage,
const pxr::SdfPathVector& selectedPaths,
const pxr::SdfPath& activeCameraPath,
const pxr::GfMatrix4d& viewProjMatrix,
double viewportCameraDist)
{
if (!stage) return;
if (!m_bboxProgram || !m_camWireVAO || !m_camWireVBO) return;
if (!m_drawTarget) return;
// Rebuild camera path list fresh each call.
m_cachedCameraPaths.clear();
for (const pxr::UsdPrim& prim : stage->Traverse()) {
if (prim.IsA<pxr::UsdGeomCamera>())
m_cachedCameraPaths.push_back(prim.GetPath());
}
if (m_cachedCameraPaths.empty()) return;
double rawScale = viewportCameraDist * 0.12;
double scale = std::min(rawScale, 50.0);
scale = std::max(scale, 0.5); // minimum visible size
// Build set of selected paths for O(1) lookup
std::unordered_set<std::string> selectedSet;
for (const auto& p : selectedPaths) selectedSet.insert(p.GetString());
// Re-bind FBO
pxr::GfVec2i sz = m_drawTarget->GetSize();
m_drawTarget->Bind();
glViewport(0, 0, sz[0], sz[1]);
// Save GL state
GLboolean prevDepthTest = glIsEnabled(GL_DEPTH_TEST);
GLboolean prevDepthMask;
glGetBooleanv(GL_DEPTH_WRITEMASK, &prevDepthMask);
GLboolean prevBlend = glIsEnabled(GL_BLEND);
// Camera wireframes draw on top of everything (like the axis gizmo) so they
// are always visible regardless of scene geometry at the camera location.
glDisable(GL_DEPTH_TEST);
glDepthMask(GL_FALSE);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
if (m_aaEnabled) glEnable(GL_LINE_SMOOTH);
glUseProgram(m_bboxProgram);
pxr::GfMatrix4f vp = pxr::GfMatrix4f(viewProjMatrix);
std::vector<float> guideVerts; // accumulates frustum oracle lines
for (const auto& camPath : m_cachedCameraPaths) {
pxr::UsdPrim prim = stage->GetPrimAtPath(camPath);
if (!prim) continue;
pxr::UsdGeomCamera usdCam(prim);
pxr::GfCamera gfCam = usdCam.GetCamera(m_currentTime);
// Determine colour
bool isSelected = (selectedSet.count(camPath.GetString()) > 0);
bool isActive = (camPath == activeCameraPath && !activeCameraPath.IsEmpty());
pxr::GfVec4f col;
if (isSelected) col = pxr::GfVec4f(1.0f, 0.75f, 0.10f, 1.0f);
else if (isActive) col = pxr::GfVec4f(0.2f, 0.90f, 1.00f, 1.0f);
else col = pxr::GfVec4f(0.65f, 0.85f, 1.00f, 0.90f); // light blue
glUniform4f(m_bboxUniformColor, col[0], col[1], col[2], col[3]);
std::vector<float> verts;
BuildCameraWireframeLines(gfCam, scale, verts);
if (verts.empty()) continue;
int vertCount = static_cast<int>(verts.size() / 3);
// Upload to the dedicated camera wireframe VBO
glBindBuffer(GL_ARRAY_BUFFER, m_camWireVBO);
glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(verts.size() * sizeof(float)),
verts.data(), GL_DYNAMIC_DRAW);
glUniformMatrix4fv(m_bboxUniformMVP, 1, GL_TRUE, vp.GetArray());
glBindVertexArray(m_camWireVAO);
glDrawArrays(GL_LINES, 0, vertCount);
glBindVertexArray(0);
// Accumulate full-frustum oracle lines for non-active cameras (stageView.DrawCameraGuides)
if (m_showCameraGuide && !isActive)
BuildCameraGuideLines(gfCam, guideVerts);
}
// Draw all camera guide frustum boxes in one call (oracle orange from stageView.py)
if (m_showCameraGuide && !guideVerts.empty()) {
glUniform4f(m_bboxUniformColor, 0.82745f, 0.39608f, 0.16471f, 1.0f);
glBindBuffer(GL_ARRAY_BUFFER, m_camWireVBO);
glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(guideVerts.size() * sizeof(float)),
guideVerts.data(), GL_DYNAMIC_DRAW);
glUniformMatrix4fv(m_bboxUniformMVP, 1, GL_TRUE, vp.GetArray());
glBindVertexArray(m_camWireVAO);
glDrawArrays(GL_LINES, 0, static_cast<int>(guideVerts.size() / 3));
glBindVertexArray(0);
}
// Restore GL state
if (m_aaEnabled) glDisable(GL_LINE_SMOOTH);
if (prevDepthTest) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST);
glDepthMask(prevDepthMask);
if (!prevBlend) glDisable(GL_BLEND);
glUseProgram(0);
m_drawTarget->Unbind();
}
// ===========================================================================
// BuildLightWireframeLines
//
// Geometry is built in the light's LOCAL space (using its schema attributes in
// local units) and every vertex is pushed through localToWorld, so position,
// orientation and scale are all handled uniformly. USD lights emit along local
// -Z (rect/disk/distant); cylinder length runs along local X.
// ===========================================================================
void UsdSceneRenderer::BuildLightWireframeLines(const pxr::UsdPrim& lightPrim,
const pxr::GfMatrix4d& l2w,
double scale,
std::vector<float>& outVerts)
{
using pxr::GfVec3d;
static constexpr int kSeg = 24;
static constexpr double kTwoPi = 6.283185307179586;
auto pushLine = [&](const GfVec3d& a, const GfVec3d& b) {
GfVec3d wa = l2w.Transform(a);
GfVec3d wb = l2w.Transform(b);
outVerts.push_back((float)wa[0]); outVerts.push_back((float)wa[1]); outVerts.push_back((float)wa[2]);
outVerts.push_back((float)wb[0]); outVerts.push_back((float)wb[1]); outVerts.push_back((float)wb[2]);
};
// Circle centred at c, spanned by unit axes u,v, radius r (local space).
auto circle = [&](const GfVec3d& c, const GfVec3d& u, const GfVec3d& v, double r) {
GfVec3d prev;
for (int i = 0; i <= kSeg; ++i) {
double a = kTwoPi * double(i) / double(kSeg);
GfVec3d p = c + u * (r * std::cos(a)) + v * (r * std::sin(a));
if (i > 0) pushLine(prev, p);
prev = p;
}
};
const GfVec3d X(1,0,0), Y(0,1,0), Z(0,0,1), O(0,0,0);
const double marker = scale * 0.5; // display-scaled size for direction lines / markers
if (lightPrim.IsA<pxr::UsdLuxRectLight>()) {
float w = 1.0f, h = 1.0f;
pxr::UsdLuxRectLight rl(lightPrim);
rl.GetWidthAttr().Get(&w, m_currentTime);
rl.GetHeightAttr().Get(&h, m_currentTime);
double hw = w * 0.5, hh = h * 0.5;
GfVec3d bl(-hw,-hh,0), br(hw,-hh,0), tr(hw,hh,0), tl(-hw,hh,0);
pushLine(bl,br); pushLine(br,tr); pushLine(tr,tl); pushLine(tl,bl);
pushLine(O, GfVec3d(0,0,-marker)); // emission direction (-Z)
}
else if (lightPrim.IsA<pxr::UsdLuxDiskLight>()) {
float r = 0.5f; pxr::UsdLuxDiskLight(lightPrim).GetRadiusAttr().Get(&r, m_currentTime);
circle(O, X, Y, r);
pushLine(O, GfVec3d(0,0,-marker));
}
else if (lightPrim.IsA<pxr::UsdLuxCylinderLight>()) {
float r = 0.5f, len = 1.0f;
pxr::UsdLuxCylinderLight cl(lightPrim);
cl.GetRadiusAttr().Get(&r, m_currentTime);
cl.GetLengthAttr().Get(&len, m_currentTime);
double hx = len * 0.5;
circle(GfVec3d( hx,0,0), Y, Z, r); // end caps (in local YZ)
circle(GfVec3d(-hx,0,0), Y, Z, r);
pushLine(GfVec3d(-hx, r,0), GfVec3d(hx, r,0)); // connecting lines
pushLine(GfVec3d(-hx,-r,0), GfVec3d(hx,-r,0));
pushLine(GfVec3d(-hx,0, r), GfVec3d(hx,0, r));
pushLine(GfVec3d(-hx,0,-r), GfVec3d(hx,0,-r));
}
else if (lightPrim.IsA<pxr::UsdLuxDistantLight>()) {
// Sun: small disc facing -Z plus parallel rays along -Z.
double r = marker;
circle(O, X, Y, r);
const GfVec3d off[5] = { O, GfVec3d(r,0,0), GfVec3d(-r,0,0), GfVec3d(0,r,0), GfVec3d(0,-r,0) };
for (const auto& o : off) pushLine(o, o + GfVec3d(0,0,-marker*2.0));
}
else if (lightPrim.IsA<pxr::UsdLuxDomeLight>()) {
// Environment dome: 3 large display-scaled circles around the origin.
double r = std::max(scale, 1.0);
circle(O, X, Y, r); circle(O, X, Z, r); circle(O, Y, Z, r);
}
else if (lightPrim.IsA<pxr::UsdLuxSphereLight>()) {
float r = 0.5f; pxr::UsdLuxSphereLight sl(lightPrim);
sl.GetRadiusAttr().Get(&r, m_currentTime);
bool pointish = false; sl.GetTreatAsPointAttr().Get(&pointish, m_currentTime);
if (pointish || r < 1e-4f) {
double s = marker; // point light: axis cross + tiny disc
pushLine(GfVec3d(-s,0,0), GfVec3d(s,0,0));
pushLine(GfVec3d(0,-s,0), GfVec3d(0,s,0));
pushLine(GfVec3d(0,0,-s), GfVec3d(0,0,s));
circle(O, X, Y, s * 0.4);
} else {
circle(O, X, Y, r); circle(O, X, Z, r); circle(O, Y, Z, r);
}
}
else {
double s = marker; // unknown light: generic point marker
pushLine(GfVec3d(-s,0,0), GfVec3d(s,0,0));
pushLine(GfVec3d(0,-s,0), GfVec3d(0,s,0));
pushLine(GfVec3d(0,0,-s), GfVec3d(0,0,s));
}
}
// ===========================================================================
// StageHasAuthoredLights
// ===========================================================================
bool UsdSceneRenderer::StageHasAuthoredLights() const
{
if (!m_stage) return false;
for (const pxr::UsdPrim& prim : m_stage->Traverse()) {
if (prim.HasAPI<pxr::UsdLuxLightAPI>())
return true;
}
return false;
}
// ===========================================================================
// DrawLightWireframes (mirrors DrawCameraWireframes)
// ===========================================================================
void UsdSceneRenderer::DrawLightWireframes(
pxr::UsdStageRefPtr stage,
const pxr::SdfPathVector& selectedPaths,
const pxr::GfMatrix4d& viewProjMatrix,
double viewportCameraDist)
{
if (!stage) return;
if (!m_bboxProgram || !m_lightWireVAO || !m_lightWireVBO) return;
if (!m_drawTarget) return;
// Collect every light prim (anything carrying UsdLuxLightAPI).
pxr::SdfPathVector lightPaths;
for (const pxr::UsdPrim& prim : stage->Traverse()) {
if (prim.HasAPI<pxr::UsdLuxLightAPI>())
lightPaths.push_back(prim.GetPath());
}
if (lightPaths.empty()) return;
double rawScale = viewportCameraDist * 0.12;
double scale = std::min(rawScale, 50.0);
scale = std::max(scale, 0.5);
std::unordered_set<std::string> selectedSet;
for (const auto& p : selectedPaths) selectedSet.insert(p.GetString());
pxr::GfVec2i sz = m_drawTarget->GetSize();
m_drawTarget->Bind();
glViewport(0, 0, sz[0], sz[1]);
GLboolean prevDepthTest = glIsEnabled(GL_DEPTH_TEST);
GLboolean prevDepthMask; glGetBooleanv(GL_DEPTH_WRITEMASK, &prevDepthMask);
GLboolean prevBlend = glIsEnabled(GL_BLEND);
// Draw on top of everything (like camera wireframes) so lights stay visible.
glDisable(GL_DEPTH_TEST);
glDepthMask(GL_FALSE);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
if (m_aaEnabled) glEnable(GL_LINE_SMOOTH);
glUseProgram(m_bboxProgram);
pxr::GfMatrix4f vp = pxr::GfMatrix4f(viewProjMatrix);
pxr::UsdGeomXformCache xformCache(m_currentTime);
for (const auto& lpath : lightPaths) {
pxr::UsdPrim prim = stage->GetPrimAtPath(lpath);
if (!prim) continue;
pxr::GfMatrix4d l2w = xformCache.GetLocalToWorldTransform(prim);
bool isSelected = (selectedSet.count(lpath.GetString()) > 0);
pxr::GfVec4f col = isSelected
? pxr::GfVec4f(1.00f, 0.75f, 0.10f, 1.00f) // accent orange
: pxr::GfVec4f(1.00f, 0.90f, 0.35f, 0.90f); // warm yellow
glUniform4f(m_bboxUniformColor, col[0], col[1], col[2], col[3]);
std::vector<float> verts;
BuildLightWireframeLines(prim, l2w, scale, verts);
if (verts.empty()) continue;
int vertCount = static_cast<int>(verts.size() / 3);
glBindBuffer(GL_ARRAY_BUFFER, m_lightWireVBO);
glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(verts.size() * sizeof(float)),
verts.data(), GL_DYNAMIC_DRAW);
glUniformMatrix4fv(m_bboxUniformMVP, 1, GL_TRUE, vp.GetArray());
glBindVertexArray(m_lightWireVAO);
glDrawArrays(GL_LINES, 0, vertCount);
glBindVertexArray(0);
}
if (m_aaEnabled) glDisable(GL_LINE_SMOOTH);
if (prevDepthTest) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST);
glDepthMask(prevDepthMask);
if (!prevBlend) glDisable(GL_BLEND);
glUseProgram(0);
m_drawTarget->Unbind();
}
// ===========================================================================
// PickCameraAtPoint
// ===========================================================================
bool UsdSceneRenderer::PickCameraAtPoint(
pxr::UsdStageRefPtr stage,
float mouseX, float mouseY,
const pxr::GfMatrix4d& viewProjMatrix,
float imagePosX, float imagePosY,
int viewW, int viewH,
double viewportCameraDist,
pxr::SdfPath* outCameraPath)
{
if (!stage || !outCameraPath) return false;
// Rebuild cache fresh (same logic as DrawCameraWireframes — always traverse
// so newly created cameras are immediately pickable).
m_cachedCameraPaths.clear();
for (const pxr::UsdPrim& prim : stage->Traverse()) {
if (prim.IsA<pxr::UsdGeomCamera>())
m_cachedCameraPaths.push_back(prim.GetPath());
}
if (m_cachedCameraPaths.empty()) return false;
static constexpr float kPickRadius = 10.0f;
double rawScale = viewportCameraDist * 0.12;
double scale = std::min(rawScale, 50.0);
scale = std::max(scale, 0.05);
float bestDist = kPickRadius;
pxr::SdfPath bestPath;
for (const auto& camPath : m_cachedCameraPaths) {
pxr::UsdPrim prim = stage->GetPrimAtPath(camPath);
if (!prim) continue;
pxr::UsdGeomCamera usdCam(prim);
pxr::GfCamera gfCam = usdCam.GetCamera(m_currentTime);
std::vector<float> verts;
BuildCameraWireframeLines(gfCam, scale, verts);
// verts = interleaved XYZ pairs (2 verts per segment = 6 floats per segment)
for (size_t i = 0; i + 5 < verts.size(); i += 6) {
pxr::GfVec3d wa(verts[i], verts[i+1], verts[i+2]);
pxr::GfVec3d wb(verts[i+3], verts[i+4], verts[i+5]);
float ax, ay, bx, by;
bool okA = WorldToScreen(wa, viewProjMatrix, viewW, viewH, imagePosX, imagePosY, ax, ay);
bool okB = WorldToScreen(wb, viewProjMatrix, viewW, viewH, imagePosX, imagePosY, bx, by);
if (!okA && !okB) continue;
if (!okA) { ax = bx; ay = by; }
if (!okB) { bx = ax; by = ay; }
float d = PointToSegmentDist(mouseX, mouseY, ax, ay, bx, by);
if (d < bestDist) {
bestDist = d;
bestPath = camPath;
}
}
}
if (bestPath.IsEmpty()) return false;
*outCameraPath = bestPath;
return true;
}
} // namespace UsdLayerManager