09819091c4
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>
1915 lines
74 KiB
C++
1915 lines
74 KiB
C++
#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
|