Add light shape wireframe display in viewport

Draw a type-appropriate wireframe gizmo for every UsdLux light in the stage,
mirroring the existing camera-wireframe overlay (dedicated VAO/VBO reusing the
bbox shader, drawn on top with depth-test off).

Shapes, built in the light's local space and pushed through its local-to-world
transform:
  - SphereLight   3 orthogonal circles (radius); axis-cross when treatAsPoint
  - RectLight     width x height rectangle + emission direction line
  - DiskLight     circle + direction line
  - CylinderLight two end-cap circles + connectors (radius/length)
  - DistantLight  sun disc + parallel rays
  - DomeLight     3 large display-scaled circles

Selected lights draw accent orange, others warm yellow.

Verified by creating a SphereLight (3-circle sphere) and a RectLight
(rectangle) in-app: distinct shapes, correct selected/deselected colours,
gizmos visible over geometry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 07:56:47 +08:00
parent a24eff04bd
commit 2649fe814c
3 changed files with 247 additions and 0 deletions
+220
View File
@@ -8,6 +8,14 @@
#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>
@@ -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<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));
}
}
// ===========================================================================
// 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
// ===========================================================================
+24
View File
@@ -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<float>& 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<float>& 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;
+3
View File
@@ -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)