mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
ifcviewer: draw 3D pivot indicator during navigation
A small RGB axis cross is rendered at camera_target_ while the user is orbiting, panning, or has just zoomed. Visibility toggles on middle-mouse press/release; the wheel arms a single-shot QTimer that hides it 750 ms after the last notch. Drawn in two passes: GL_GREATER at 30% alpha for the occluded portion (X-ray cue) and GL_LEQUAL at full alpha for the visible portion. Arm length is computed from camera_distance_, fovy, and viewport height so the cross stays ~30 px on screen across zoom levels. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
#include <QSurfaceFormat>
|
||||
#include <QCoreApplication>
|
||||
#include <QCursor>
|
||||
#include <QTimer>
|
||||
#include <QtMath>
|
||||
#include <QtOpenGL/QOpenGLVersionFunctionsFactory>
|
||||
|
||||
@@ -236,6 +237,35 @@ out vec4 frag_color;
|
||||
void main() { frag_color = vec4(v_color, 1.0); }
|
||||
)";
|
||||
|
||||
// Pivot indicator: a small 3D axis cross drawn at u_pivot in world space.
|
||||
// a_local is the unit endpoint of one of the six arm vertices (±X,±Y,±Z),
|
||||
// scaled by u_arm_world so the cross keeps a roughly constant on-screen size
|
||||
// across zoom levels. Drawing in world space (not screen-aligned) is the
|
||||
// whole point: it tumbles visibly when the camera orbits, which makes it
|
||||
// unambiguous that the marker is anchored to a 3D point and not glued to
|
||||
// the screen. Per-vertex color gives RGB axes matching the corner gizmo.
|
||||
static const char* PIVOT_VERTEX_SHADER = R"(
|
||||
#version 450 core
|
||||
layout(location = 0) in vec3 a_local;
|
||||
layout(location = 1) in vec3 a_color;
|
||||
uniform mat4 u_mvp;
|
||||
uniform vec3 u_pivot;
|
||||
uniform float u_arm_world;
|
||||
out vec3 v_color;
|
||||
void main() {
|
||||
gl_Position = u_mvp * vec4(u_pivot + a_local * u_arm_world, 1.0);
|
||||
v_color = a_color;
|
||||
}
|
||||
)";
|
||||
|
||||
static const char* PIVOT_FRAGMENT_SHADER = R"(
|
||||
#version 450 core
|
||||
in vec3 v_color;
|
||||
uniform float u_alpha;
|
||||
out vec4 frag_color;
|
||||
void main() { frag_color = vec4(v_color, u_alpha); }
|
||||
)";
|
||||
|
||||
static GLuint compileShader(QOpenGLFunctions_4_5_Core* gl, GLenum type, const char* source) {
|
||||
GLuint shader = gl->glCreateShader(type);
|
||||
gl->glShaderSource(shader, 1, &source, nullptr);
|
||||
@@ -434,9 +464,12 @@ ViewportWindow::~ViewportWindow() {
|
||||
}
|
||||
if (axis_vao_) gl_->glDeleteVertexArrays(1, &axis_vao_);
|
||||
if (axis_vbo_) gl_->glDeleteBuffers(1, &axis_vbo_);
|
||||
if (pivot_vao_) gl_->glDeleteVertexArrays(1, &pivot_vao_);
|
||||
if (pivot_vbo_) gl_->glDeleteBuffers(1, &pivot_vbo_);
|
||||
if (main_program_) gl_->glDeleteProgram(main_program_);
|
||||
if (pick_program_) gl_->glDeleteProgram(pick_program_);
|
||||
if (axis_program_) gl_->glDeleteProgram(axis_program_);
|
||||
if (pivot_program_) gl_->glDeleteProgram(pivot_program_);
|
||||
if (pick_fbo_) gl_->glDeleteFramebuffers(1, &pick_fbo_);
|
||||
if (pick_color_tex_) gl_->glDeleteTextures(1, &pick_color_tex_);
|
||||
if (pick_depth_rbo_) gl_->glDeleteRenderbuffers(1, &pick_depth_rbo_);
|
||||
@@ -464,6 +497,7 @@ void ViewportWindow::initGL() {
|
||||
|
||||
buildShaders();
|
||||
buildAxisGizmo();
|
||||
buildPivotIndicator();
|
||||
|
||||
gl_->glEnable(GL_DEPTH_TEST);
|
||||
gl_->glEnable(GL_MULTISAMPLE);
|
||||
@@ -570,6 +604,11 @@ void ViewportWindow::buildShaders() {
|
||||
GLuint fs = compileShader(gl_, GL_FRAGMENT_SHADER, AXIS_FRAGMENT_SHADER);
|
||||
axis_program_ = linkProgram(gl_, vs, fs);
|
||||
}
|
||||
{
|
||||
GLuint vs = compileShader(gl_, GL_VERTEX_SHADER, PIVOT_VERTEX_SHADER);
|
||||
GLuint fs = compileShader(gl_, GL_FRAGMENT_SHADER, PIVOT_FRAGMENT_SHADER);
|
||||
pivot_program_ = linkProgram(gl_, vs, fs);
|
||||
}
|
||||
{
|
||||
GLuint vs = compileShader(gl_, GL_VERTEX_SHADER, HIZ_DOWNSAMPLE_VS);
|
||||
GLuint fs = compileShader(gl_, GL_FRAGMENT_SHADER, HIZ_DOWNSAMPLE_FS);
|
||||
@@ -599,6 +638,31 @@ void ViewportWindow::buildAxisGizmo() {
|
||||
gl_->glVertexArrayAttribBinding(axis_vao_, 1, 0);
|
||||
}
|
||||
|
||||
void ViewportWindow::buildPivotIndicator() {
|
||||
// 6 verts = 3 line segments along world ±X, ±Y, ±Z, RGB-coded.
|
||||
static const float verts[] = {
|
||||
// local x,y,z r,g,b
|
||||
-1, 0, 0, 1.0f, 0.30f, 0.30f,
|
||||
1, 0, 0, 1.0f, 0.30f, 0.30f,
|
||||
0,-1, 0, 0.35f, 0.95f, 0.35f,
|
||||
0, 1, 0, 0.35f, 0.95f, 0.35f,
|
||||
0, 0,-1, 0.35f, 0.55f, 1.0f,
|
||||
0, 0, 1, 0.35f, 0.55f, 1.0f,
|
||||
};
|
||||
pivot_rim_count_ = 6; // total vertex count drawn as GL_LINES
|
||||
|
||||
gl_->glCreateVertexArrays(1, &pivot_vao_);
|
||||
gl_->glCreateBuffers(1, &pivot_vbo_);
|
||||
gl_->glNamedBufferStorage(pivot_vbo_, sizeof(verts), verts, 0);
|
||||
gl_->glVertexArrayVertexBuffer(pivot_vao_, 0, pivot_vbo_, 0, 6 * sizeof(float));
|
||||
gl_->glEnableVertexArrayAttrib(pivot_vao_, 0);
|
||||
gl_->glVertexArrayAttribFormat(pivot_vao_, 0, 3, GL_FLOAT, GL_FALSE, 0);
|
||||
gl_->glVertexArrayAttribBinding(pivot_vao_, 0, 0);
|
||||
gl_->glEnableVertexArrayAttrib(pivot_vao_, 1);
|
||||
gl_->glVertexArrayAttribFormat(pivot_vao_, 1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float));
|
||||
gl_->glVertexArrayAttribBinding(pivot_vao_, 1, 0);
|
||||
}
|
||||
|
||||
bool ViewportWindow::growModelVbo(ModelGpuData& m, size_t needed_total) {
|
||||
size_t new_capacity = m.vbo_capacity;
|
||||
while (new_capacity < needed_total) new_capacity *= 2;
|
||||
@@ -2082,6 +2146,7 @@ void ViewportWindow::render() {
|
||||
hiz_reject_count_.load());
|
||||
gl_->glBindBuffer(GL_DRAW_INDIRECT_BUFFER, 0);
|
||||
|
||||
renderPivotIndicator();
|
||||
renderAxisGizmo();
|
||||
|
||||
// Build HiZ from this frame's resolved depth for next frame's cull.
|
||||
@@ -2418,6 +2483,69 @@ void ViewportWindow::renderPickPass() {
|
||||
gl_->glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
void ViewportWindow::renderPivotIndicator() {
|
||||
if (!pivot_indicator_visible_ || !pivot_program_ || !pivot_vao_) return;
|
||||
|
||||
const int h = height() * devicePixelRatio();
|
||||
if (h <= 0) return;
|
||||
|
||||
// Pick a world-space arm length that projects to ~30 pixels at the pivot's
|
||||
// distance. At fovy=45°, half the visible world height at distance d is
|
||||
// d * tan(22.5°) ≈ 0.4142 * d. pixels_per_world = (h/2) / (0.4142 * d).
|
||||
// Inverting: world_per_pixel = 0.8284 * d / h.
|
||||
const float fovy_rad = qDegreesToRadians(camera_fov_y_deg_);
|
||||
const float world_per_pixel = camera_distance_ * tanf(fovy_rad * 0.5f) * 2.0f / float(h);
|
||||
const float arm_pixels = 30.0f * float(devicePixelRatio());
|
||||
const float arm_world = arm_pixels * world_per_pixel;
|
||||
|
||||
const QMatrix4x4 mvp = proj_matrix_ * view_matrix_;
|
||||
|
||||
gl_->glUseProgram(pivot_program_);
|
||||
gl_->glUniformMatrix4fv(gl_->glGetUniformLocation(pivot_program_, "u_mvp"),
|
||||
1, GL_FALSE, mvp.constData());
|
||||
gl_->glUniform3f(gl_->glGetUniformLocation(pivot_program_, "u_pivot"),
|
||||
camera_target_.x(), camera_target_.y(), camera_target_.z());
|
||||
gl_->glUniform1f(gl_->glGetUniformLocation(pivot_program_, "u_arm_world"),
|
||||
arm_world);
|
||||
|
||||
gl_->glEnable(GL_BLEND);
|
||||
gl_->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
gl_->glLineWidth(2.0f);
|
||||
gl_->glBindVertexArray(pivot_vao_);
|
||||
|
||||
// Pass 1: occluded portions, depth test reversed, dim — gives an X-ray
|
||||
// hint that the pivot lives behind geometry.
|
||||
gl_->glDepthFunc(GL_GREATER);
|
||||
gl_->glUniform1f(gl_->glGetUniformLocation(pivot_program_, "u_alpha"), 0.30f);
|
||||
gl_->glDrawArrays(GL_LINES, 0, pivot_rim_count_);
|
||||
|
||||
// Pass 2: visible portions, normal depth, full alpha.
|
||||
gl_->glDepthFunc(GL_LEQUAL);
|
||||
gl_->glUniform1f(gl_->glGetUniformLocation(pivot_program_, "u_alpha"), 1.0f);
|
||||
gl_->glDrawArrays(GL_LINES, 0, pivot_rim_count_);
|
||||
|
||||
gl_->glBindVertexArray(0);
|
||||
gl_->glDisable(GL_BLEND);
|
||||
gl_->glDepthFunc(GL_LESS);
|
||||
}
|
||||
|
||||
void ViewportWindow::setPivotIndicatorVisible(bool visible, int hide_after_ms) {
|
||||
if (!pivot_indicator_hide_timer_) {
|
||||
pivot_indicator_hide_timer_ = new QTimer(this);
|
||||
pivot_indicator_hide_timer_->setSingleShot(true);
|
||||
connect(pivot_indicator_hide_timer_, &QTimer::timeout, this, [this]() {
|
||||
pivot_indicator_visible_ = false;
|
||||
requestUpdate();
|
||||
});
|
||||
}
|
||||
pivot_indicator_visible_ = visible;
|
||||
if (visible && hide_after_ms > 0) {
|
||||
pivot_indicator_hide_timer_->start(hide_after_ms);
|
||||
} else {
|
||||
pivot_indicator_hide_timer_->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportWindow::renderAxisGizmo() {
|
||||
if (!axis_program_ || !axis_vao_) return;
|
||||
const int dpr = devicePixelRatio();
|
||||
@@ -2475,6 +2603,10 @@ void ViewportWindow::handleMousePress(QMouseEvent* e) {
|
||||
}
|
||||
active_button_ = e->button();
|
||||
last_mouse_pos_ = e->pos();
|
||||
if (e->button() == Qt::MiddleButton) {
|
||||
setPivotIndicatorVisible(true);
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
void ViewportWindow::handleMouseRelease(QMouseEvent* e) {
|
||||
if (camera_mode_ == CameraMode::Fps) return;
|
||||
@@ -2484,7 +2616,12 @@ void ViewportWindow::handleMouseRelease(QMouseEvent* e) {
|
||||
emit objectPicked(id);
|
||||
requestUpdate(); // selection highlight changed
|
||||
}
|
||||
const bool was_navigating = (active_button_ == Qt::MiddleButton);
|
||||
active_button_ = Qt::NoButton;
|
||||
if (was_navigating && pivot_indicator_visible_) {
|
||||
setPivotIndicatorVisible(false);
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
void ViewportWindow::handleMouseMove(QMouseEvent* e) {
|
||||
if (camera_mode_ == CameraMode::Fps) {
|
||||
@@ -2550,5 +2687,6 @@ void ViewportWindow::handleWheel(QWheelEvent* e) {
|
||||
float factor = e->angleDelta().y() > 0 ? 0.9f : 1.1f;
|
||||
camera_distance_ *= factor;
|
||||
camera_distance_ = qMax(0.1f, camera_distance_);
|
||||
setPivotIndicatorVisible(true, 750);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
#include <QVector3D>
|
||||
#include <QSet>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QTimer;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <cstdint>
|
||||
@@ -231,9 +235,16 @@ private:
|
||||
void render();
|
||||
void renderPickPass();
|
||||
void renderAxisGizmo();
|
||||
void renderPivotIndicator();
|
||||
void updateCamera();
|
||||
void buildShaders();
|
||||
void buildAxisGizmo();
|
||||
void buildPivotIndicator();
|
||||
// Show/hide the orbit-pivot marker. hide_after_ms > 0 starts a single-shot
|
||||
// timer that auto-hides — used by the wheel handler to give the marker a
|
||||
// short afterglow after zoom. Drag-based callers pass 0 and toggle
|
||||
// visibility manually on press/release.
|
||||
void setPivotIndicatorVisible(bool visible, int hide_after_ms = 0);
|
||||
void setupVaoLayout(GLuint vao, GLuint vbo, GLuint ebo);
|
||||
|
||||
// Resolve the default framebuffer's MSAA depth into a single-sample
|
||||
@@ -315,6 +326,14 @@ private:
|
||||
GLuint axis_vao_ = 0;
|
||||
GLuint axis_vbo_ = 0;
|
||||
|
||||
// Orbit-pivot indicator: 1 center vertex + (N+1) rim vertices on the unit
|
||||
// circle (last == first to close the triangle fan). Rendered as a screen-
|
||||
// space disc at camera_target_, visible only while the user is navigating.
|
||||
GLuint pivot_program_ = 0;
|
||||
GLuint pivot_vao_ = 0;
|
||||
GLuint pivot_vbo_ = 0;
|
||||
int pivot_rim_count_ = 0;
|
||||
|
||||
// Per-model GPU data
|
||||
std::unordered_map<uint32_t, ModelGpuData> models_gpu_;
|
||||
|
||||
@@ -411,6 +430,11 @@ private:
|
||||
Qt::MouseButton active_button_ = Qt::NoButton;
|
||||
QPoint last_mouse_pos_;
|
||||
|
||||
// Pivot indicator visibility — true while drag-navigating, or briefly
|
||||
// after a wheel notch (the timer auto-clears it).
|
||||
bool pivot_indicator_visible_ = false;
|
||||
QTimer* pivot_indicator_hide_timer_ = nullptr;
|
||||
|
||||
// FPS/fly mode state. fps_keys_held_ tracks WASD/QE/Shift between
|
||||
// press+release; fps_last_tick_ gates dt inside render().
|
||||
// fps_ignore_next_mouse_move_ swallows the synthetic MouseMove that
|
||||
|
||||
Reference in New Issue
Block a user