File gl_widget.cpp
File List > gui > gl_widget.cpp
Go to the documentation of this file
#include "gl_widget.hpp"
#include <math.h>
#include <QCoreApplication>
#include <QMatrix4x4>
#include <QMouseEvent>
#include <QOpenGLFunctions>
#include <QOpenGLShaderProgram>
#include <QPainter>
#include <QVector3D>
#include <QVector4D>
#include <QtWidgets>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <limits>
#include <vector>
#include "gui/gl_check.hpp"
namespace {
double point_separation_m(const PointPickResult& a, const PointPickResult& b) {
const double dx = a.world_x - b.world_x;
const double dy = a.world_y - b.world_y;
const double dz = a.world_z - b.world_z;
return std::sqrt(dx * dx + dy * dy + dz * dz);
}
float point_diameter_px(const Camera& camera, float viewport_height, qreal dpr,
const PointPickResult& pick,
const std::vector<std::shared_ptr<Layer>>& layers) {
const QVector3D pt(static_cast<float>(pick.world_x), static_cast<float>(pick.world_y),
static_cast<float>(pick.world_z));
const float fb_viewport_h = viewport_height * static_cast<float>(dpr);
const float proj_scale =
fb_viewport_h / static_cast<float>(2.0 * std::tan(camera.fov_rad() * 0.5));
float diameter_px = 1.0f / static_cast<float>(dpr);
for (const auto& layer : layers) {
if (layer->name() != pick.layer_name) {
continue;
}
if (auto las_layer = std::dynamic_pointer_cast<LASLayer>(layer)) {
const QVector4D eye = camera.view_matrix() * QVector4D(pt, 1.0f);
const float eye_w = -eye.z();
if (eye_w > 0) {
const float diameter_device =
std::clamp(2.0f * las_layer->point_radius_m() * proj_scale / eye_w, 1.0f, 4096.0f);
diameter_px = diameter_device / static_cast<float>(dpr);
}
}
break;
}
return diameter_px;
}
QPointF snap_device_center(const QPointF& pt, qreal dpr) {
return QPointF((std::floor(pt.x() * dpr) + 0.5) / dpr, (std::floor(pt.y() * dpr) + 0.5) / dpr);
}
QVector3D normalize_or_default(const QVector3D& value, const QVector3D& fallback) {
if (value.lengthSquared() < 1e-8f) {
return fallback;
}
return value.normalized();
}
float clamp_float(float value, float min_v, float max_v) {
return std::max(min_v, std::min(max_v, value));
}
QVector3D light_lub_from_polar(float azimuth_deg, float elevation_deg) {
const float azimuth_rad = static_cast<float>(azimuth_deg * M_PI / 180.0);
const float elevation_rad = static_cast<float>(elevation_deg * M_PI / 180.0);
const float horizontal = std::cos(elevation_rad);
const float left = -std::sin(azimuth_rad) * horizontal;
const float up = std::sin(elevation_rad);
const float behind = std::cos(azimuth_rad) * horizontal;
return normalize_or_default(QVector3D(left, up, behind), QVector3D(0.35f, 0.45f, 0.82f));
}
void draw_device_hline(QPainter& painter, qreal x0, qreal x1, qreal y, qreal dpr,
const QColor& color) {
const qreal px = 1.0 / dpr;
const qreal cy = (std::floor(y * dpr) + 0.5) / dpr;
const qreal left = std::floor(std::min(x0, x1) * dpr) / dpr;
const qreal right = std::ceil(std::max(x0, x1) * dpr) / dpr;
painter.fillRect(QRectF(left, cy - px * 0.5, right - left, px), color);
}
void draw_device_vline(QPainter& painter, qreal x, qreal y0, qreal y1, qreal dpr,
const QColor& color) {
const qreal px = 1.0 / dpr;
const qreal cx = (std::floor(x * dpr) + 0.5) / dpr;
const qreal top = std::floor(std::min(y0, y1) * dpr) / dpr;
const qreal bottom = std::ceil(std::max(y0, y1) * dpr) / dpr;
painter.fillRect(QRectF(cx - px * 0.5, top, px, bottom - top), color);
}
void draw_point_crosshair(QPainter& painter, const std::optional<QPointF>& screen_pt,
const Camera& camera, float viewport_height, const PointPickResult& pick,
const std::vector<std::shared_ptr<Layer>>& layers, qreal dpr,
const QColor& cross_color,
std::optional<double> distance_m = std::nullopt) {
if (!screen_pt.has_value()) {
return;
}
const QPointF& pt = *screen_pt;
const QPointF center = snap_device_center(pt, dpr);
const float point_px = point_diameter_px(camera, viewport_height, dpr, pick, layers);
const float gap_px = point_px * 0.5f + 1.0f;
const float arm_len = (12.0f + point_px) * 0.5f;
const float arm_px = gap_px + arm_len;
painter.save();
painter.setRenderHint(QPainter::Antialiasing, false);
painter.setPen(Qt::NoPen);
draw_device_hline(painter, center.x() + gap_px, center.x() + arm_px, center.y(), dpr,
cross_color);
draw_device_hline(painter, center.x() - gap_px, center.x() - arm_px, center.y(), dpr,
cross_color);
draw_device_vline(painter, center.x(), center.y() + gap_px, center.y() + arm_px, dpr,
cross_color);
draw_device_vline(painter, center.x(), center.y() - gap_px, center.y() - arm_px, dpr,
cross_color);
painter.restore();
if (distance_m.has_value()) {
painter.setPen(QColor(100, 200, 255));
painter.drawText(center + QPointF(arm_px + 4, -4), QString("%1 m").arg(*distance_m, 0, 'f', 2));
}
}
} // namespace
GLWidget::GLWidget(QWidget* parent) : QOpenGLWidget(parent), m_camera(width(), height()) {
setMouseTracking(true);
m_idle_timer = new QTimer(this);
m_idle_timer->setSingleShot(true);
m_idle_timer->setInterval(50);
connect(m_idle_timer, &QTimer::timeout, this, &GLWidget::on_camera_idle);
m_stream_timer = new QTimer(this);
m_stream_timer->setInterval(16);
connect(m_stream_timer, &QTimer::timeout, this, &GLWidget::on_stream_tick);
m_orbit_timer = new QTimer(this);
m_orbit_timer->setInterval(16);
connect(m_orbit_timer, &QTimer::timeout, this, &GLWidget::on_orbit_tick);
}
GLWidget::~GLWidget() {
makeCurrent();
m_renderers.clear();
doneCurrent();
}
void GLWidget::remove_layer(Layer* layer) {
const auto it =
std::find_if(m_layers.begin(), m_layers.end(),
[layer](const std::shared_ptr<Layer>& ptr) { return ptr.get() == layer; });
if (it == m_layers.end()) {
return;
}
const size_t index = static_cast<size_t>(std::distance(m_layers.begin(), it));
Layer* layer_ptr = it->get();
LayerRenderer* renderer = m_renderers[index].get();
disconnect(layer_ptr, nullptr, renderer, nullptr);
disconnect(renderer, nullptr, this, nullptr);
disconnect(layer_ptr, nullptr, this, nullptr);
m_zoomed_layers.erase(layer_ptr);
clear_picks_for_layer(layer_ptr->name());
if (auto* las = dynamic_cast<OctreeLASLayerRenderer*>(renderer)) {
const int slot = las->layer_slot();
las->set_layer_slot(0);
if (slot > 0) {
release_layer_slot(slot);
}
}
makeCurrent();
m_renderers.erase(m_renderers.begin() + static_cast<std::ptrdiff_t>(index));
m_layers.erase(it);
restart_render();
update();
}
QSize GLWidget::sizeHint() const { return minimumSizeHint(); }
QSize GLWidget::minimumSizeHint() const { return QSize(320, 240); }
void GLWidget::initializeGL() {
initializeOpenGLFunctions();
setFocusPolicy(Qt::StrongFocus);
QOpenGLFunctions* f = QOpenGLContext::currentContext()->functions();
std::cout << "Detected OpenGL version: " << f->glGetString(GL_VERSION) << std::endl;
std::cout << "Detected OpenGL renderer: " << f->glGetString(GL_RENDERER) << std::endl;
Assert(f->glGetError() == GL_NO_ERROR, "OpenGL error");
Assert(QOpenGLContext::currentContext()->isValid(), "OpenGL context is invalid");
CHECK_GL(f->glEnable(GL_PROGRAM_POINT_SIZE));
CHECK_GL(f->glEnable(GL_DEPTH_TEST));
QPair<int, int> version = QOpenGLContext::currentContext()->format().version();
if (version.first < 3 || (version.first == 3 && version.second < 3)) {
CHECK_GL(f->glEnable(GL_POINT_SPRITE));
}
CHECK_GL(f->glEnable(GL_BLEND));
CHECK_GL(f->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
CHECK_GL(f->glClearColor(0.2f, 0.2f, 0.2f, 1.0f));
QSurfaceFormat format = QOpenGLContext::currentContext()->format();
if (format.profile() == QSurfaceFormat::CompatibilityProfile) {
QOpenGLFunctions* opengl_functions = QOpenGLContext::currentContext()->functions();
GLint point_sprite_coord_origin = 0;
CHECK_GL(
opengl_functions->glGetIntegerv(GL_POINT_SPRITE_COORD_ORIGIN, &point_sprite_coord_origin));
AssertEQ(point_sprite_coord_origin, GL_UPPER_LEFT);
}
GLenum err;
while ((err = glGetError()) != GL_NO_ERROR) {
std::cout << "OpenGL Initialization Error: " << err << std::endl;
}
}
void GLWidget::showEvent(QShowEvent* event) {
QOpenGLWidget::showEvent(event);
apply_pending_zoom();
update();
}
void GLWidget::resizeGL(int w, int h) {
Q_UNUSED(w);
Q_UNUSED(h);
// resizeGL receives device pixels; camera and input use logical widget pixels.
const int screen_w = width();
const int screen_h = height();
if (screen_w > 0 && screen_h > 0) {
m_camera.set_screen_size(screen_w, screen_h);
apply_pending_zoom();
restart_render();
}
}
void GLWidget::restart_render() {
m_incremental_draw = false;
if (m_painting) {
m_restarted_during_paint = true;
}
}
void GLWidget::update_scene_bounds() {
Extent3D bounds;
for (const auto& layer : m_layers) {
if (!layer->visible() || layer->extent().max_extent() <= 0) {
continue;
}
bounds.grow(layer->extent() - m_camera.world_offset());
}
if (bounds.max_extent() <= 0) {
m_camera.set_scene_bounds(QVector3D(), 0.0f);
return;
}
const QVector3D center(static_cast<float>(0.5 * (bounds.minx + bounds.maxx)),
static_cast<float>(0.5 * (bounds.miny + bounds.maxy)),
static_cast<float>(0.5 * (bounds.minz + bounds.maxz)));
const double dx = bounds.maxx - bounds.minx;
const double dy = bounds.maxy - bounds.miny;
const double dz = bounds.maxz - bounds.minz;
const float radius = static_cast<float>(0.5 * std::sqrt(dx * dx + dy * dy + dz * dz));
m_camera.set_scene_bounds(center, radius);
}
void GLWidget::paintGL() {
if (m_painting) {
return;
}
m_painting = true;
const auto paint_start = std::chrono::steady_clock::now();
if (m_prev_paint_time) {
m_last_present_frame_ms =
std::chrono::duration<double, std::milli>(paint_start - *m_prev_paint_time).count();
m_present_frame_ms_ema = m_present_frame_ms_ema * 0.88 + m_last_present_frame_ms * 0.12;
}
m_prev_paint_time = paint_start;
QOpenGLFunctions* f = QOpenGLContext::currentContext()->functions();
const int screen_w = width();
const int screen_h = height();
const qreal dpr = devicePixelRatioF();
const int fb_w = std::max(1, static_cast<int>(std::lround(screen_w * dpr)));
const int fb_h = std::max(1, static_cast<int>(std::lround(screen_h * dpr)));
if (screen_w > 0 && screen_h > 0) {
m_camera.set_screen_size(screen_w, screen_h);
update_scene_bounds();
apply_pending_zoom();
m_scene_fbo.ensure_size(fb_w, fb_h);
m_points_fbo.ensure_size(fb_w, fb_h);
}
if (screen_w <= 0 || screen_h <= 0) {
m_painting = false;
return;
}
const bool incremental_points = m_incremental_draw;
RenderContext ctx;
ctx.incremental_points = incremental_points;
ctx.viewport_height = static_cast<float>(fb_h);
const QVector3D camera_forward = normalize_or_default(m_camera.direction(), QVector3D(0, 0, -1));
const QVector3D camera_up = normalize_or_default(m_camera.up(), QVector3D(0, 0, 1));
QVector3D camera_right = QVector3D::crossProduct(camera_forward, camera_up);
camera_right = normalize_or_default(camera_right, QVector3D(1, 0, 0));
const QVector3D ortho_up = normalize_or_default(
QVector3D::crossProduct(camera_right, camera_forward), QVector3D(0, 0, 1));
const QVector3D light_lub = light_lub_from_polar(m_light_azimuth_deg, m_light_elevation_deg);
const QVector3D light_world =
-light_lub.x() * camera_right + light_lub.y() * ortho_up - light_lub.z() * camera_forward;
const QVector3D light_eye(-light_lub.x(), light_lub.y(), light_lub.z());
ctx.light_direction_world = normalize_or_default(light_world, QVector3D(0, 0, 1));
ctx.light_direction_eye = normalize_or_default(light_eye, QVector3D(0, 0, 1));
ctx.ambient_light = m_ambient_light;
ctx.diffuse_light = m_diffuse_light;
ctx.point_ambient_light = m_point_ambient_light;
CHECK_GL(f->glClearColor(0.2f, 0.2f, 0.2f, 1.0f));
if (!incremental_points) {
if (m_scene_fbo.valid()) {
m_scene_fbo.bind();
CHECK_GL_AFTER();
CHECK_GL(f->glViewport(0, 0, fb_w, fb_h));
CHECK_GL(f->glClearColor(0.0f, 0.0f, 0.0f, 0.0f));
CHECK_GL(f->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
} else {
CHECK_GL(f->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
}
for (size_t i = 0; i < m_renderers.size(); ++i) {
if (m_layers[i]->kind() == LayerKind::PointCloud) {
continue;
}
if (m_renderers[i] && !m_load_only) {
m_renderers[i]->render(m_camera, ctx);
}
}
if (m_points_fbo.valid()) {
m_points_fbo.bind();
CHECK_GL_AFTER();
CHECK_GL(f->glViewport(0, 0, fb_w, fb_h));
m_points_fbo.clear();
}
} else if (m_points_fbo.valid()) {
m_points_fbo.bind();
CHECK_GL_AFTER();
CHECK_GL(f->glViewport(0, 0, fb_w, fb_h));
CHECK_GL(f->glDepthFunc(GL_LEQUAL));
}
for (size_t i = 0; i < m_renderers.size(); ++i) {
if (m_layers[i]->kind() != LayerKind::PointCloud) {
continue;
}
if (m_renderers[i]) {
m_renderers[i]->render(m_camera, ctx);
}
}
const GLuint widget_fbo = defaultFramebufferObject();
CHECK_GL(f->glBindFramebuffer(GL_FRAMEBUFFER, widget_fbo));
CHECK_GL(f->glViewport(0, 0, fb_w, fb_h));
CHECK_GL(f->glClearColor(0.2f, 0.2f, 0.2f, 1.0f));
CHECK_GL(f->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
if (m_scene_fbo.valid()) {
if (auto* gl = QOpenGLContext::currentContext()->extraFunctions()) {
m_scene_fbo.composite_to_widget_fbo(gl, widget_fbo, fb_w, fb_h);
m_scene_fbo.blit_depth_to_widget_fbo(widget_fbo, fb_w, fb_h);
CHECK_GL_AFTER();
}
}
const float point_layer_alpha = this->point_layer_alpha();
if (m_points_fbo.valid() && point_layer_alpha > 0.0f) {
if (auto* gl = QOpenGLContext::currentContext()->extraFunctions()) {
m_point_compositor.composite(gl, widget_fbo, m_points_fbo.color_texture(),
m_points_fbo.depth_texture(), 1.0f, fb_w, fb_h);
CHECK_GL_AFTER();
}
}
const bool pick_ready = fbo_pick_ready() && point_layer_alpha > 0.0f;
if (!pick_ready && m_fbo_was_pick_ready) {
apply_hover_result(std::nullopt, /*repaint=*/false);
m_last_hover_probe_pos.reset();
}
if (pick_ready && !m_fbo_was_pick_ready) {
m_hover_probe_needed = true;
}
m_fbo_was_pick_ready = pick_ready;
if (m_pending_pick_pixel && pick_ready) {
do_pick_at(*m_pending_pick_pixel, m_pending_pick_action, /*repaint=*/false);
m_pending_pick_pixel.reset();
m_pending_pick_action = PickAction::None;
}
// Probe hover after compositing so pick reads match the displayed frame.
if (pick_ready && !m_left_button_pressed && !m_camera_interacting) {
const bool cursor_moved =
!m_last_hover_probe_pos.has_value() || *m_last_hover_probe_pos != m_last_mouse_pos;
if (cursor_moved || m_hover_probe_needed) {
update_hover_at(m_last_mouse_pos, /*max_radius=*/5, /*repaint=*/false);
m_last_hover_probe_pos = m_last_mouse_pos;
m_hover_probe_needed = false;
}
}
bool point_streaming = false;
bool layer_loading = false;
for (size_t i = 0; i < m_renderers.size(); ++i) {
if (m_layers[i]->kind() != LayerKind::PointCloud) {
continue;
}
if (auto* las_renderer = dynamic_cast<OctreeLASLayerRenderer*>(m_renderers[i].get())) {
if (las_renderer->has_stream_backlog()) {
point_streaming = true;
}
}
if (auto las_layer = std::dynamic_pointer_cast<LASLayer>(m_layers[i])) {
if (!las_layer->las_data().load_complete()) {
layer_loading = true;
}
}
}
if (point_streaming || layer_loading) {
if (!m_stream_timer->isActive()) {
m_stream_timer->start();
}
} else if (!m_camera_interacting) {
m_stream_timer->stop();
}
// Inspired by Displaz: next frame accumulates unless restart_render() is called.
if (!m_camera_interacting && !m_restarted_during_paint) {
m_incremental_draw = true;
}
m_restarted_during_paint = false;
m_last_point_draw_ms = 0.0;
m_last_point_gpu_ms = 0.0;
m_last_point_vertices = 0;
for (size_t i = 0; i < m_renderers.size(); ++i) {
if (m_layers[i]->kind() != LayerKind::PointCloud) {
continue;
}
if (auto* las_renderer = dynamic_cast<OctreeLASLayerRenderer*>(m_renderers[i].get())) {
m_last_point_draw_ms += las_renderer->last_point_draw_ms();
m_last_point_gpu_ms += las_renderer->last_point_gpu_ms();
m_last_point_vertices += las_renderer->last_point_vertices_drawn();
}
}
m_last_paint_ms =
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - paint_start)
.count();
// Periodic frame benchmark (every 60 frames).
{
static int bench_frame = 0;
if (++bench_frame % 60 == 0) {
const double fps = m_present_frame_ms_ema > 0.0 ? 1000.0 / m_present_frame_ms_ema : 0.0;
std::cerr << "[blaze bench frame] fps=" << fps << " present_ms=" << m_last_present_frame_ms
<< " paint_ms=" << m_last_paint_ms << " gpu_ms=" << m_last_point_gpu_ms
<< " pts_ms=" << m_last_point_draw_ms << " verts=" << m_last_point_vertices
<< " cam=(" << m_camera.position().x() << "," << m_camera.position().y() << ","
<< m_camera.position().z() << ")" << std::endl;
}
}
// Poll errors from our GL rendering before QPainter — QPainter does not
// preserve OpenGL state and may itself enqueue GL_INVALID_OPERATION on core
// profiles, which would otherwise drown out real issues.
GLenum err;
while ((err = glGetError()) != GL_NO_ERROR) {
std::cerr << "OpenGL error: " << gl_error_name(err) << " (0x" << std::hex << err << std::dec
<< ")" << std::endl;
}
draw_stats_overlay();
// QPainter does not preserve framebuffer bindings; restore for next frame.
CHECK_GL(f->glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebufferObject()));
m_painting = false;
}
bool GLWidget::fbo_pick_ready() const {
// Pick/hover reads the accumulated points FBO. That buffer is cleared only
// on restart_render() (camera interaction). Incremental streaming appends
// more points each frame without invalidating existing pick IDs, so we must
// not gate on m_incremental_draw — paintGL sets that true after every idle
// frame, which would block hover until the camera moves again.
if (m_camera_interacting) {
return false;
}
bool any_point_layer = false;
for (size_t i = 0; i < m_renderers.size(); ++i) {
if (m_layers[i]->kind() != LayerKind::PointCloud || !m_layers[i]->visible()) {
continue;
}
const auto* las = dynamic_cast<const OctreeLASLayerRenderer*>(m_renderers[i].get());
if (!las) {
continue;
}
any_point_layer = true;
if (!las->can_fbo_pick()) {
return false;
}
}
return any_point_layer;
}
float GLWidget::point_layer_alpha() const {
float alpha = 0.0f;
for (size_t i = 0; i < m_layers.size(); ++i) {
if (m_layers[i]->kind() != LayerKind::PointCloud || !m_layers[i]->visible()) {
continue;
}
if (auto las_layer = std::dynamic_pointer_cast<LASLayer>(m_layers[i])) {
alpha = std::max(alpha, las_layer->point_alpha());
}
}
return alpha;
}
bool GLWidget::pick_interaction_enabled() const {
return fbo_pick_ready() && point_layer_alpha() > 0.0f;
}
void GLWidget::draw_stats_overlay() {
QPainter painter(this);
painter.setRenderHint(QPainter::TextAntialiasing);
const double fps = m_present_frame_ms_ema > 0.0 ? 1000.0 / m_present_frame_ms_ema : 0.0;
const QString text = tr("%1 fps %2 ms %3 ms gpu %4k verts")
.arg(fps, 0, 'f', 0)
.arg(m_last_present_frame_ms, 0, 'f', 1)
.arg(m_last_point_gpu_ms, 0, 'f', 1)
.arg(static_cast<double>(m_last_point_vertices) / 1000.0, 0, 'f', 1);
const QFontMetrics fm(painter.font());
const int pad_x = 8;
const int pad_y = 4;
const int text_w = fm.horizontalAdvance(text);
const int text_h = fm.height();
const int box_w = text_w + pad_x * 2;
const int box_h = text_h + pad_y * 2;
const int margin = 10;
const QRect box_rect(width() - box_w - margin, height() - box_h - margin, box_w, box_h);
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(0, 0, 0, 150));
painter.drawRoundedRect(box_rect, 4, 4);
painter.setPen(QColor(230, 230, 230));
painter.drawText(box_rect.adjusted(pad_x, pad_y, -pad_x, -pad_y),
Qt::AlignLeft | Qt::AlignVCenter, text);
const auto layer_exists = [&](const std::string& name) {
return std::any_of(m_layers.begin(), m_layers.end(),
[&](const std::shared_ptr<Layer>& layer) { return layer->name() == name; });
};
// Crosshairs anchored to the picked point's screen position (not the cursor).
const float logical_viewport_h = static_cast<float>(height());
const qreal dpr = devicePixelRatioF();
if (m_selected_point.has_value() && layer_exists(m_selected_point->layer_name)) {
if (auto screen_pt = project_pick_to_screen(*m_selected_point)) {
draw_point_crosshair(painter, screen_pt, m_camera, logical_viewport_h, *m_selected_point,
m_layers, dpr, QColor(255, 235, 40));
}
}
if (m_hovered_point.has_value() && m_hovered_point != m_selected_point &&
layer_exists(m_hovered_point->layer_name)) {
if (auto screen_pt = project_pick_to_screen(*m_hovered_point)) {
std::optional<double> sep;
if (m_selected_point.has_value()) {
sep = point_separation_m(*m_selected_point, *m_hovered_point);
}
draw_point_crosshair(painter, screen_pt, m_camera, logical_viewport_h, *m_hovered_point,
m_layers, dpr, QColor(80, 220, 255), sep);
}
}
}
void GLWidget::begin_camera_interaction() {
if (!m_camera_interacting) {
m_stream_timer->stop();
restart_render();
}
m_camera_interacting = true;
m_hover_probe_needed = false;
m_hovered_point.reset();
m_last_hover_probe_pos.reset();
}
void GLWidget::note_camera_motion() {
begin_camera_interaction();
schedule_camera_idle();
}
void GLWidget::schedule_camera_idle() { m_idle_timer->start(); }
void GLWidget::on_camera_idle() {
m_camera_interacting = false;
m_hover_probe_needed = true;
if (!m_stream_timer->isActive()) {
m_stream_timer->start();
}
update();
}
void GLWidget::on_stream_tick() {
if (!m_camera_interacting) {
update();
}
}
void GLWidget::refresh_point_cloud_style() {
for (size_t i = 0; i < m_renderers.size(); ++i) {
if (m_layers[i]->kind() != LayerKind::PointCloud) {
continue;
}
if (auto* las_renderer = dynamic_cast<OctreeLASLayerRenderer*>(m_renderers[i].get())) {
las_renderer->refresh_after_style_change();
}
}
restart_render();
if (!m_stream_timer->isActive()) {
m_stream_timer->start();
}
update();
}
void GLWidget::set_camera_light_angles(float azimuth_deg, float elevation_deg) {
const float clamped_azimuth = clamp_float(azimuth_deg, -180.0f, 180.0f);
const float clamped_elevation = clamp_float(elevation_deg, -89.0f, 89.0f);
if (std::abs(clamped_azimuth - m_light_azimuth_deg) < 1e-6f &&
std::abs(clamped_elevation - m_light_elevation_deg) < 1e-6f) {
return;
}
m_light_azimuth_deg = clamped_azimuth;
m_light_elevation_deg = clamped_elevation;
restart_render();
update();
}
void GLWidget::set_lighting_strength(float ambient_light, float diffuse_light) {
const float clamped_ambient = clamp_float(ambient_light, 0.0f, 1.0f);
const float clamped_diffuse = clamp_float(diffuse_light, 0.0f, 2.0f);
if (std::abs(clamped_ambient - m_ambient_light) < 1e-6f &&
std::abs(clamped_diffuse - m_diffuse_light) < 1e-6f) {
return;
}
m_ambient_light = clamped_ambient;
m_diffuse_light = clamped_diffuse;
restart_render();
update();
}
void GLWidget::set_point_ambient_light(float point_ambient_light) {
const float clamped_point_ambient = clamp_float(point_ambient_light, 0.0f, 1.0f);
if (std::abs(clamped_point_ambient - m_point_ambient_light) < 1e-6f) {
return;
}
m_point_ambient_light = clamped_point_ambient;
restart_render();
update();
}
void GLWidget::pan_to_selected_point() {
if (!m_selected_point.has_value()) {
return;
}
m_camera.pan_to_target(QVector3D(static_cast<float>(m_selected_point->world_x),
static_cast<float>(m_selected_point->world_y),
static_cast<float>(m_selected_point->world_z)));
restart_render();
update();
}
void GLWidget::look_at_selected_point() {
if (!m_selected_point.has_value()) {
return;
}
m_camera.look_at_target(QVector3D(static_cast<float>(m_selected_point->world_x),
static_cast<float>(m_selected_point->world_y),
static_cast<float>(m_selected_point->world_z)));
restart_render();
update();
}
void GLWidget::clear_picks_for_layer(const std::string& layer_name) {
if (m_selected_point.has_value() && m_selected_point->layer_name == layer_name) {
set_selected_point(std::nullopt, false);
if (m_point_pick_callback) {
m_point_pick_callback(std::nullopt);
}
}
if (m_hovered_point.has_value() && m_hovered_point->layer_name == layer_name) {
m_hovered_point.reset();
}
}
std::optional<QPointF> GLWidget::project_pick_to_screen(const PointPickResult& pick) const {
return m_camera.project_world_to_screen(QVector3D(static_cast<float>(pick.world_x),
static_cast<float>(pick.world_y),
static_cast<float>(pick.world_z)));
}
int GLWidget::allocate_layer_slot() {
if (!m_free_layer_slots.empty()) {
const int slot = m_free_layer_slots.back();
m_free_layer_slots.pop_back();
return slot;
}
if (m_next_layer_slot > 255) {
std::cerr << "Warning: Maximum of 255 point cloud layers with picking support reached. "
"Additional layers will not support point picking.\n";
return 0;
}
return m_next_layer_slot++;
}
void GLWidget::release_layer_slot(int slot) {
if (slot > 0) {
m_free_layer_slots.push_back(slot);
}
}
void GLWidget::set_selected_point(const std::optional<PointPickResult>& pick, bool repaint) {
m_selected_point = pick;
if (repaint) {
update();
}
}
void GLWidget::try_pick_point(const QPointF& pixel, PickAction action) {
m_pending_pick_pixel = pixel;
m_pending_pick_action = action;
update();
}
void GLWidget::do_pick_at(const QPointF& pixel, PickAction action, bool repaint) {
if (!pick_interaction_enabled()) {
return;
}
auto best = fb_pick_point(pixel, /*max_radius=*/5);
if (best) {
set_selected_point(*best, repaint);
if (m_point_pick_callback) {
m_point_pick_callback(*best);
}
if (action == PickAction::PanToPoint) {
pan_to_selected_point();
} else if (action == PickAction::LookAtPoint) {
look_at_selected_point();
}
} else {
set_selected_point(std::nullopt, repaint);
if (m_point_pick_callback) {
m_point_pick_callback(std::nullopt);
}
}
}
void GLWidget::apply_hover_result(const std::optional<PointPickResult>& result, bool repaint) {
if (result == m_hovered_point) {
return;
}
m_hovered_point = result;
if (repaint) {
update();
}
}
void GLWidget::update_hover_at(const QPointF& pixel, int max_radius, bool repaint) {
if (!pick_interaction_enabled()) {
if (m_hovered_point.has_value()) {
apply_hover_result(std::nullopt, repaint);
}
return;
}
apply_hover_result(fb_pick_point(pixel, max_radius), repaint);
}
std::optional<PointPickResult> GLWidget::fb_pick_point(const QPointF& pixel, int max_radius) {
if (!m_points_fbo.valid()) {
return std::nullopt;
}
makeCurrent();
auto* ef = QOpenGLContext::currentContext()->extraFunctions();
if (!ef) {
return std::nullopt;
}
GLint prev_read_fbo = 0;
GLint prev_read_buffer = 0;
CHECK_GL(ef->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prev_read_fbo));
CHECK_GL(ef->glGetIntegerv(GL_READ_BUFFER, &prev_read_buffer));
const qreal dpr = devicePixelRatioF();
const int fb_w = static_cast<int>(std::lround(width() * dpr));
const int fb_h = static_cast<int>(std::lround(height() * dpr));
const int cx = static_cast<int>(std::floor(pixel.x() * dpr + 0.5));
const int cy = fb_h - static_cast<int>(std::floor(pixel.y() * dpr + 0.5)) - 1;
const int pick_radius = std::max(1, static_cast<int>(std::lround(max_radius * dpr)));
const GLuint widget_fbo = defaultFramebufferObject();
const int x0 = std::max(0, cx - pick_radius);
const int y0 = std::max(0, cy - pick_radius);
const int x1 = std::min(fb_w - 1, cx + pick_radius);
const int y1 = std::min(fb_h - 1, cy + pick_radius);
const int region_w = x1 - x0 + 1;
const int region_h = y1 - y0 + 1;
const size_t pixel_count = static_cast<size_t>(region_w) * static_cast<size_t>(region_h);
std::vector<uint32_t> pick_pixels(pixel_count * 2);
std::vector<float> point_depths(pixel_count);
std::vector<float> widget_depths(pixel_count);
m_points_fbo.bind_read();
CHECK_GL(ef->glReadBuffer(GL_COLOR_ATTACHMENT1));
CHECK_GL(ef->glReadPixels(x0, y0, region_w, region_h, GL_RG_INTEGER, GL_UNSIGNED_INT,
pick_pixels.data()));
CHECK_GL(ef->glBindFramebuffer(GL_READ_FRAMEBUFFER, m_points_fbo.fbo()));
CHECK_GL(ef->glReadPixels(x0, y0, region_w, region_h, GL_DEPTH_COMPONENT, GL_FLOAT,
point_depths.data()));
CHECK_GL(ef->glBindFramebuffer(GL_READ_FRAMEBUFFER, widget_fbo));
CHECK_GL(ef->glReadPixels(x0, y0, region_w, region_h, GL_DEPTH_COMPONENT, GL_FLOAT,
widget_depths.data()));
struct PickCandidate {
uint32_t pick_index = 0;
uint32_t slot = 0;
float widget_depth = 1.0f;
int screen_dist_sq = 0;
};
std::optional<PickCandidate> best;
for (int row = 0; row < region_h; ++row) {
for (int col = 0; col < region_w; ++col) {
const int sx = x0 + col;
const int sy = y0 + row;
const size_t idx =
static_cast<size_t>(row) * static_cast<size_t>(region_w) + static_cast<size_t>(col);
const uint32_t pick_index = pick_pixels[idx * 2];
const uint32_t slot = pick_pixels[idx * 2 + 1];
if (pick_index == 0 || slot == 0) {
continue;
}
const float widget_depth = widget_depths[idx];
if (widget_depth >= 0.99999f) {
continue;
}
if (point_depths[idx] > widget_depth + 2e-4f) {
continue;
}
const int pdx = sx - cx;
const int pdy = sy - cy;
const int screen_dist_sq = pdx * pdx + pdy * pdy;
if (!best || screen_dist_sq < best->screen_dist_sq ||
(screen_dist_sq == best->screen_dist_sq && widget_depth < best->widget_depth)) {
best = PickCandidate{pick_index, slot, widget_depth, screen_dist_sq};
}
}
}
CHECK_GL(ef->glBindFramebuffer(GL_READ_FRAMEBUFFER, prev_read_fbo));
CHECK_GL(ef->glReadBuffer(prev_read_buffer));
if (!best) {
return std::nullopt;
}
for (size_t i = 0; i < m_renderers.size(); ++i) {
if (m_layers[i]->kind() != LayerKind::PointCloud || !m_layers[i]->visible()) {
continue;
}
auto* las = dynamic_cast<OctreeLASLayerRenderer*>(m_renderers[i].get());
if (!las || las->layer_slot() != static_cast<int>(best->slot)) {
continue;
}
if (!las->can_fbo_pick()) {
continue;
}
return las->point_from_index(best->slot, best->pick_index, m_camera.world_offset());
}
return std::nullopt;
}
void GLWidget::mousePressEvent(QMouseEvent* event) {
m_last_mouse_pos = event->position();
m_press_mouse_pos = m_last_mouse_pos;
if (event->button() == Qt::LeftButton) {
m_left_button_pressed = true;
}
// Defer begin_camera_interaction() for left button until the first drag pixel.
// restart_render() clears the points FBO; starting orbit on press would wipe
// pick IDs before a click-release can read them from the accumulated buffer.
if (event->button() == Qt::MiddleButton || event->button() == Qt::RightButton) {
begin_camera_interaction();
}
}
void GLWidget::mouseReleaseEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton && m_left_button_pressed) {
const QPointF release_pos = event->position();
const qreal drag_dx = release_pos.x() - m_press_mouse_pos.x();
const qreal drag_dy = release_pos.y() - m_press_mouse_pos.y();
if (drag_dx * drag_dx + drag_dy * drag_dy <= 36) {
PickAction action = PickAction::None;
if (event->modifiers().testFlag(Qt::ShiftModifier)) {
action = PickAction::PanToPoint;
} else if (event->modifiers().testFlag(Qt::ControlModifier)) {
action = PickAction::LookAtPoint;
}
try_pick_point(release_pos, action);
}
m_left_button_pressed = false;
}
schedule_camera_idle();
}
void GLWidget::mouseMoveEvent(QMouseEvent* event) {
const QPointF pos = event->position();
const qreal dx = pos.x() - m_last_mouse_pos.x();
const qreal dy = pos.y() - m_last_mouse_pos.y();
if (event->buttons() & Qt::LeftButton) {
if (!m_camera_interacting && (std::abs(dx) > 0 || std::abs(dy) > 0)) {
begin_camera_interaction();
}
m_camera.rotate_around_center(0.28 * dx, 0.28 * dy);
} else if (event->buttons() & Qt::MiddleButton) {
m_camera.rotate_view(0.28 * dx, 0.28 * dy);
} else if (event->buttons() & Qt::RightButton) {
m_camera.pan(dx / 320.0f, dy / 320.0f);
}
m_last_mouse_pos = pos;
if (event->buttons() != Qt::NoButton) {
if (std::abs(dx) > 0 || std::abs(dy) > 0) {
note_camera_motion();
} else {
schedule_camera_idle();
}
// Clear hover during drag
if (m_hovered_point.has_value()) {
m_hovered_point.reset();
}
update();
} else if (!m_camera_interacting) {
if (std::abs(dx) > 0 || std::abs(dy) > 0) {
m_hover_probe_needed = true;
}
update();
}
}
void GLWidget::leaveEvent(QEvent* event) {
Q_UNUSED(event);
if (m_hovered_point.has_value()) {
apply_hover_result(std::nullopt, /*repaint=*/false);
}
m_last_hover_probe_pos.reset();
update();
}
void GLWidget::wheelEvent(QWheelEvent* event) {
m_last_mouse_pos = event->position();
note_camera_motion();
m_hovered_point.reset();
m_last_hover_probe_pos.reset();
QVector3D world_pos = m_camera.unproject(event->position());
m_camera.move_towards(world_pos,
m_camera.direction().length() * event->angleDelta().y() / 2000.0f, true);
update();
}
void GLWidget::keyPressEvent(QKeyEvent* event) {
if (event->key() == Qt::Key_W) {
m_camera.fly(1 / 20., 0, 0);
} else if (event->key() == Qt::Key_S) {
m_camera.fly(-1 / 20., 0, 0);
} else if (event->key() == Qt::Key_A) {
m_camera.fly(0, 1 / 20., 0);
} else if (event->key() == Qt::Key_D) {
m_camera.fly(0, -1 / 20., 0);
} else if (event->key() == Qt::Key_Q) {
m_camera.fly(0, 0, -1 / 10.);
} else if (event->key() == Qt::Key_E) {
m_camera.fly(0, 0, 1 / 10.);
} else if (event->key() == Qt::Key_R) {
m_camera.reset_to_origin();
} else if (event->key() == Qt::Key_F) {
Extent3D bounds;
for (const auto& layer : m_layers) {
bounds.grow(layer->extent() - m_camera.world_offset());
}
if (bounds.max_extent() > 0) {
request_zoom_to_extent(bounds);
}
} else if (event->key() == Qt::Key_Z && m_selected_point.has_value()) {
pan_to_selected_point();
return;
} else if (event->key() == Qt::Key_Space) {
if (m_orbit_timer->isActive())
stop_animation();
else
start_animation(m_last_anim_type);
return;
} else {
event->ignore();
return;
}
note_camera_motion();
update();
}
void GLWidget::set_anim_type(int t) {
AnimType type = static_cast<AnimType>(t);
if (type != AnimType::None) m_last_anim_type = type;
if (type == m_anim_type) return;
m_anim_type = type;
if (type == AnimType::None)
stop_animation();
else
start_animation(type);
}
void GLWidget::start_animation(AnimType type) {
if (m_layers.empty()) return;
m_anim_type = type;
m_last_orbit_tick = std::chrono::steady_clock::now();
m_anim_phase = 0.0;
restart_render();
m_orbit_timer->start();
update();
}
void GLWidget::stop_animation() {
m_anim_type = AnimType::None;
m_orbit_timer->stop();
update();
}
void GLWidget::start_bench_orbit(double duration_seconds) {
start_animation(AnimType::Orbit);
QTimer::singleShot(static_cast<int>(duration_seconds * 1000), [] { std::exit(0); });
}
void GLWidget::on_orbit_tick() {
if (m_layers.empty() || m_anim_type == AnimType::None) return;
auto now = std::chrono::steady_clock::now();
double dt = std::chrono::duration<double>(now - m_last_orbit_tick).count();
m_last_orbit_tick = now;
constexpr double FULL_CIRCLE_DEG = 360.0;
double period = (m_anim_type == AnimType::Orbit) ? m_orbit_period_secs : m_wobble_period_secs;
double degrees = FULL_CIRCLE_DEG * dt / period;
m_anim_phase += 2.0 * 3.1415926535 * dt / period; // radians
switch (m_anim_type) {
case AnimType::Orbit:
m_camera.rotate_around_center(degrees, 0);
break;
case AnimType::Wobble: {
// Wobble traces a circle with angular radius = amplitude (degrees).
// Per-tick rotation must be scaled by dt/period to convert position to velocity.
double dphase = 2.0 * 3.1415926535 * dt / period;
double h = m_wobble_amplitude_deg * dphase * std::cos(m_anim_phase);
double v = -m_wobble_amplitude_deg * dphase * std::sin(m_anim_phase);
m_camera.rotate_around_center(h, v);
break;
}
case AnimType::None:
break;
}
restart_render();
update();
}