diff --git a/src/core/UsdSceneRenderer.cpp b/src/core/UsdSceneRenderer.cpp index 843ba03..0ce0a57 100644 --- a/src/core/UsdSceneRenderer.cpp +++ b/src/core/UsdSceneRenderer.cpp @@ -8,6 +8,14 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -160,6 +168,7 @@ UsdSceneRenderer::~UsdSceneRenderer() { DestroyAxisResources(); DestroyBBoxResources(); DestroyCamWireResources(); + DestroyLightWireResources(); } // =========================================================================== @@ -177,6 +186,7 @@ void UsdSceneRenderer::SetStage(pxr::UsdStageRefPtr stage) { DestroyAxisResources(); DestroyBBoxResources(); DestroyCamWireResources(); + DestroyLightWireResources(); // Determine stage up-axis for dome light rotation (mirrors stageView._stageIsZup) if (stage) { @@ -252,6 +262,7 @@ void UsdSceneRenderer::InitRenderer() { InitAxisResources(); InitBBoxResources(); InitCamWireResources(); + InitLightWireResources(); } // =========================================================================== @@ -1046,6 +1057,30 @@ void UsdSceneRenderer::DestroyCamWireResources() 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) @@ -1392,6 +1427,191 @@ void UsdSceneRenderer::DrawCameraWireframes( 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& 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()) { + 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()) { + 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()) { + 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()) { + // 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()) { + // 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()) { + 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)); + } +} + +// =========================================================================== +// 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()) + 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 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 verts; + BuildLightWireframeLines(prim, l2w, scale, verts); + if (verts.empty()) continue; + + int vertCount = static_cast(verts.size() / 3); + glBindBuffer(GL_ARRAY_BUFFER, m_lightWireVBO); + glBufferData(GL_ARRAY_BUFFER, + static_cast(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 // =========================================================================== diff --git a/src/core/UsdSceneRenderer.h b/src/core/UsdSceneRenderer.h index 9d8549e..bda9210 100644 --- a/src/core/UsdSceneRenderer.h +++ b/src/core/UsdSceneRenderer.h @@ -108,6 +108,16 @@ public: const pxr::GfMatrix4d& viewProjMatrix, double viewportCameraDist); + /// Draw wireframe gizmos for all UsdLux light prims in the stage. + /// The shape reflects each light's type (sphere/rect/disk/distant/dome/ + /// cylinder); selected lights get an accent colour. viewportCameraDist + /// scales direction lines and point/distant/dome markers. + void DrawLightWireframes( + pxr::UsdStageRefPtr stage, + const pxr::SdfPathVector& selectedPaths, + const pxr::GfMatrix4d& viewProjMatrix, + double viewportCameraDist); + /// Test a screen-space mouse position against all camera wireframe segments. /// Returns true and sets *outCameraPath if a camera wireframe is within /// kCameraPickRadius (10 px) of mousePos. Prioritises the closest hit. @@ -217,6 +227,8 @@ private: void DestroyBBoxResources(); void InitCamWireResources(); void DestroyCamWireResources(); + void InitLightWireResources(); + void DestroyLightWireResources(); /// Draw a single axis-aligned box from a GfRange3d. void DrawBox(const pxr::GfRange3d& range, const pxr::GfMatrix4f& mvp); @@ -227,6 +239,14 @@ private: double scale, std::vector& outVerts); + /// Build light-gizmo line segments (GL_LINES vertex pairs) in world space for + /// a single light prim. localToWorld is the light's transform at m_currentTime; + /// scale is the display-size factor (for direction lines / point markers). + void BuildLightWireframeLines(const pxr::UsdPrim& lightPrim, + const pxr::GfMatrix4d& localToWorld, + double scale, + std::vector& outVerts); + /// Project a world-space point to absolute screen coordinates (x=imagePosX+pixelX, etc.). /// Returns false when the point is behind the camera. static bool WorldToScreen(const pxr::GfVec3d& world, @@ -307,6 +327,10 @@ private: GLuint m_camWireVAO = 0; GLuint m_camWireVBO = 0; ///< dynamic VBO; reallocated per frame as needed + // --- Light wireframe GL resources (mirrors camera wireframe) --- + GLuint m_lightWireVAO = 0; + GLuint m_lightWireVBO = 0; ///< dynamic VBO; reallocated per frame as needed + // --- Camera wireframe cache --- pxr::SdfPathVector m_cachedCameraPaths; bool m_cameraCacheDirty = true; diff --git a/src/ui/ViewportTile.cpp b/src/ui/ViewportTile.cpp index cdf2a16..1757e8b 100644 --- a/src/ui/ViewportTile.cpp +++ b/src/ui/ViewportTile.cpp @@ -1104,6 +1104,9 @@ void ViewportTile::Render(int tileIndex, ImVec2 pos, ImVec2 size, m_stage, m_selectedSdfPaths, m_camera.GetUsdCameraPath(), viewProj, m_camera.GetDist()); + m_renderer.DrawLightWireframes( + m_stage, m_selectedSdfPaths, + viewProj, m_camera.GetDist()); } // Display FBO texture (flip UV Y: OpenGL is bottom-up)