Utils/CloudView3D/Src/PointCloudGLWidget.cpp

3764 lines
128 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "PointCloudGLWidget.h"
#include <QColor>
#include <QDebug>
#include <QtMath>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QGestureEvent>
#include <QHBoxLayout>
#include <QFontMetrics>
#include <QOpenGLContext>
#include <QPainter>
#include <QPinchGesture>
#include <QPushButton>
#include <QResizeEvent>
#include <QStringList>
#include <QTimer>
#include <QToolButton>
#include <QVBoxLayout>
#include <algorithm>
#include <array>
#include <cfloat>
#include <cmath>
#include <iterator>
#include <map>
#include <utility>
#define CLOUD_LOG_INFO(...) \
qInfo().noquote() << QString::asprintf(__VA_ARGS__).trimmed()
#define CLOUD_LOG_WARN(...) \
qWarning().noquote() << QString::asprintf(__VA_ARGS__).trimmed()
// OpenGL/GLU 头文件
#ifdef _WIN32
#include <windows.h>
#include <GL/gl.h>
#include <GL/glu.h>
#else
#include <GL/gl.h>
#include <GL/glu.h>
#endif
#ifndef GL_PROGRAM_POINT_SIZE
#define GL_PROGRAM_POINT_SIZE 0x8642
#endif
PointCloudGLWidget::PointCloudGLWidget(QWidget* parent)
: PointCloudGLWidget(parent, true)
{
}
namespace
{
constexpr float kDefaultPseudoColorPeriod = 500.0f;
constexpr int kPseudoColorHueCount = 360;
// 将角度归一化到 [-180, 180],避免往同一方向旋转时无限累加
float NormalizeAngleDegrees(float angle)
{
float wrapped = std::fmod(angle, 360.0f);
if (wrapped > 180.0f) {
wrapped -= 360.0f;
} else if (wrapped < -180.0f) {
wrapped += 360.0f;
}
return wrapped;
}
QVector3D clampPseudoColor(const QVector3D& color)
{
const auto clampComponent = [](float component)
{
return std::isfinite(component) ? qBound(0.0f, component, 1.0f) : 0.0f;
};
return QVector3D(clampComponent(color.x()),
clampComponent(color.y()),
clampComponent(color.z()));
}
// 颜色选择器色相条(S/V 取最大)的 360 色查找表。
const std::array<QVector3D, kPseudoColorHueCount>& defaultPseudoColorLut()
{
static const std::array<QVector3D, kPseudoColorHueCount> lut = []()
{
std::array<QVector3D, kPseudoColorHueCount> colors{};
for (int hue = 0; hue < kPseudoColorHueCount; ++hue)
{
const QColor color = QColor::fromHsv(hue, 255, 255);
colors[static_cast<size_t>(hue)] = QVector3D(color.redF(),
color.greenF(),
color.blueF());
}
return colors;
}();
return lut;
}
QVector3D defaultHuePseudoColor(float t)
{
const float clamped = qBound(0.0f, t, 1.0f);
// 与颜色选择器色相条一致:红 → 品红 → 蓝 → 青 → 绿 → 黄 → 红。
int hue = static_cast<int>(std::floor(360.0f * (1.0f - clamped))) % kPseudoColorHueCount;
if (hue < 0)
{
hue += kPseudoColorHueCount;
}
return defaultPseudoColorLut()[static_cast<size_t>(hue)];
}
}
PointCloudGLWidget::PointCloudGLWidget(QWidget* parent, bool showControlButtons)
: QOpenGLWidget(parent)
, m_shader(nullptr)
, m_shaderReady(false)
, m_mvpLocation(0)
, m_positionLocation(0)
, m_colorLocation(0)
, m_pointSizeLocation(0)
, m_alphaLocation(0)
, m_useVertexColorLocation(0)
, m_uniformColorLocation(0)
, m_distance(100.0f)
, m_pinchStartDistance(100.0f)
, m_rotationX(0.0f)
, m_rotationY(0.0f)
, m_rotationZ(0.0f)
, m_rotation(QQuaternion()) // 初始化为单位四元数
, m_center(0, 0, 0)
, m_pan(0, 0, 0)
, m_minBound(-50, -50, -50)
, m_maxBound(50, 50, 50)
, m_leftButtonPressed(false)
, m_rightButtonPressed(false)
, m_middleButtonPressed(false)
, m_currentColor(PointCloudColor::White)
, m_pointSize(1.0f)
// 纯 XYZ 点云默认启用参考色环;带 RGB 的点云仍使用原始颜色。
, m_pseudoColorEnabled(true)
, m_pseudoColorPeriod(kDefaultPseudoColorPeriod)
, m_pseudoColorStartColor(0.0f, 0.0f, 1.0f)
, m_pseudoColorEndColor(1.0f, 0.0f, 0.0f)
, m_pseudoColorDefaultPalette(true)
, m_lineSelectMode(LineSelectMode::Vertical)
, m_eulerRotationOrder(EulerRotationOrder::ZYX)
, m_measureDistanceEnabled(false)
, m_hasListHighlightPoint(false)
, m_colorIndex(0)
, m_showControlButtons(showControlButtons)
, m_controlPanel(nullptr)
, m_viewToolbar(nullptr)
, m_viewAngleButton(nullptr)
, m_zoomInButton(nullptr)
, m_zoomOutButton(nullptr)
, m_streamRefreshTimer(new QTimer(this))
, m_queuedPointCloudLineCount(0)
, m_streamDisplayedLineCount(0)
, m_streamRefreshRate(30)
, m_streamLineLimit(100)
{
setFocusPolicy(Qt::StrongFocus);
setMouseTracking(true);
setAttribute(Qt::WA_AcceptTouchEvents, true);
grabGesture(Qt::PinchGesture);
// 确保 m_model 是单位矩阵
m_model.setToIdentity();
// 默认按固定 Z 周期走颜色选择器色相条。
createOverlayControls();
connect(this, &PointCloudGLWidget::viewAnglesChanged,
this, [this](float, float, float) { updateViewAngleDisplay(); });
m_streamRefreshTimer->setTimerType(Qt::PreciseTimer);
m_streamRefreshTimer->setInterval(33);
connect(m_streamRefreshTimer, &QTimer::timeout, this, [this]()
{
// 先让数据源批量填充缓存,再在同一个 GUI tick 内提交一次显示。
emit streamRefreshRequested();
flushPointCloudBatches();
});
}
bool PointCloudGLWidget::event(QEvent* event)
{
if (event->type() == QEvent::Gesture) {
auto* gestureEvent = static_cast<QGestureEvent*>(event);
if (auto* pinch = static_cast<QPinchGesture*>(
gestureEvent->gesture(Qt::PinchGesture))) {
const bool handled = handlePinchGesture(pinch);
if (handled) {
gestureEvent->accept(pinch);
return true;
}
}
}
return QOpenGLWidget::event(event);
}
bool PointCloudGLWidget::handlePinchGesture(QPinchGesture* gesture)
{
if (!gesture) {
return false;
}
if (gesture->state() == Qt::GestureStarted) {
m_pinchStartDistance = m_distance;
}
const qreal scale = gesture->totalScaleFactor();
if (scale <= 0.0 || !std::isfinite(static_cast<double>(scale))) {
return false;
}
m_distance = qBound(minimumCameraDistance(),
m_pinchStartDistance / static_cast<float>(scale),
1.0e7f);
update();
return true;
}
PointCloudGLWidget::~PointCloudGLWidget()
{
// 在 GL 上下文中释放所有 VBO
makeCurrent();
for (auto& cloudData : m_pointClouds) {
releaseVBO(cloudData);
}
if (m_shader) {
delete m_shader;
m_shader = nullptr;
}
doneCurrent();
}
void PointCloudGLWidget::uploadToVBO(PointCloudData& data)
{
// 先释放旧的 VBO
releaseVBO(data);
if (data.vertices.empty()) {
return;
}
// 创建并上传顶点 VBO
data.vertexBuffer.create();
data.vertexBuffer.bind();
data.vertexBuffer.setUsagePattern(QOpenGLBuffer::StaticDraw);
data.vertexBuffer.allocate(data.vertices.data(),
static_cast<int>(data.vertices.size() * sizeof(float)));
data.vertexBuffer.release();
// 如果有颜色数据,创建并上传颜色 VBO
if ((data.hasColor || data.pseudoColor) && !data.colors.empty()) {
data.colorBuffer.create();
data.colorBuffer.bind();
data.colorBuffer.setUsagePattern(QOpenGLBuffer::StaticDraw);
data.colorBuffer.allocate(data.colors.data(),
static_cast<int>(data.colors.size() * sizeof(float)));
data.colorBuffer.release();
}
data.vboCreated = true;
}
void PointCloudGLWidget::releaseVBO(PointCloudData& data)
{
if (!data.vboCreated) {
return;
}
if (data.vertexBuffer.isCreated()) {
data.vertexBuffer.destroy();
}
if (data.colorBuffer.isCreated()) {
data.colorBuffer.destroy();
}
data.vboCreated = false;
}
void PointCloudGLWidget::expandCloudBounds(PointCloudData& data, float x, float y, float z)
{
if (!data.hasBounds) {
data.minBound = QVector3D(x, y, z);
data.maxBound = QVector3D(x, y, z);
data.hasBounds = true;
return;
}
data.minBound.setX(qMin(data.minBound.x(), x));
data.minBound.setY(qMin(data.minBound.y(), y));
data.minBound.setZ(qMin(data.minBound.z(), z));
data.maxBound.setX(qMax(data.maxBound.x(), x));
data.maxBound.setY(qMax(data.maxBound.y(), y));
data.maxBound.setZ(qMax(data.maxBound.z(), z));
}
void PointCloudGLWidget::recomputeCloudBounds(PointCloudData& data)
{
data.hasBounds = false;
for (size_t index = 0; index + 2 < data.vertices.size(); index += 3)
{
const float x = data.vertices[index];
const float y = data.vertices[index + 1];
const float z = data.vertices[index + 2];
if (std::isfinite(x) && std::isfinite(y) && std::isfinite(z))
{
expandCloudBounds(data, x, y, z);
}
}
}
void PointCloudGLWidget::createOverlayControls()
{
const int buttonSize = 32;
const QString toolButtonStyle =
"QToolButton {"
" background-color: rgba(20, 36, 58, 200);"
" color: #d8e8f4;"
" border: 1px solid #2b4f77;"
" border-radius: 4px;"
" font-size: 13px;"
"}"
"QToolButton:hover { background-color: #204a72; }"
"QToolButton:pressed { background-color: #10243a; }";
// ── 左侧视图方向工具栏(纵向排布) ──
struct ViewPreset {
const char* label;
const char* tooltip;
float rx, ry, rz;
};
// 坐标系定义:X向右,Y向下,Z朝后(与 CloudView 一致)
const ViewPreset presets[] = {
{"前", "正视 (XY面)", 180.0f, 0.0f, 0.0f},
{"后", "后视", 180.0f, 180.0f, 0.0f},
{"左", "左视 (YZ面)", 180.0f, 90.0f, 0.0f},
{"右", "右视", 180.0f, -90.0f, 0.0f},
{"上", "俯视 (XZ面)", 90.0f, 0.0f, 0.0f},
{"下", "仰视", -90.0f, 0.0f, 0.0f},
{"机", "机械臂", -90.0f, 90.0f, 0.0f},
};
m_viewToolbar = new QWidget(this);
m_viewToolbar->setStyleSheet("background: transparent;");
auto* toolbarLayout = new QVBoxLayout(m_viewToolbar);
toolbarLayout->setContentsMargins(4, 4, 4, 4);
toolbarLayout->setSpacing(4);
// 工具栏尺寸跟随内容,便于垂直居中定位
toolbarLayout->setSizeConstraint(QLayout::SetFixedSize);
for (const ViewPreset& preset : presets) {
auto* button = new QToolButton(m_viewToolbar);
button->setText(QString::fromUtf8(preset.label));
button->setToolTip(QString::fromUtf8(preset.tooltip));
button->setFixedSize(buttonSize, buttonSize);
button->setStyleSheet(toolButtonStyle);
const float rx = preset.rx;
const float ry = preset.ry;
const float rz = preset.rz;
connect(button, &QToolButton::clicked, this, [this, rx, ry, rz]() {
setViewAngles(rx, ry, rz);
});
m_viewButtons.append(button);
toolbarLayout->addWidget(button);
}
toolbarLayout->addStretch(1);
m_viewToolbar->setFixedWidth(buttonSize + 12);
m_viewToolbar->setVisible(m_showControlButtons);
// ── 左下角旋转角度显示按钮(点击弹窗输入角度) ──
m_viewAngleButton = new QPushButton(this);
m_viewAngleButton->setStyleSheet(
"QPushButton {"
" background-color: rgba(20, 36, 58, 200);"
" color: #d8e8f4;"
" border: 1px solid #2b4f77;"
" border-radius: 4px;"
" padding: 4px 8px;"
" text-align: left;"
"}"
"QPushButton:hover { background-color: #204a72; }");
connect(m_viewAngleButton, &QPushButton::clicked,
this, &PointCloudGLWidget::showViewAngleDialog);
// 显式指定字体,保证 fontMetrics 度量与实际渲染一致
QFont angleFont = m_viewAngleButton->font();
angleFont.setPixelSize(12);
m_viewAngleButton->setFont(angleFont);
// 竖向三行显示 RX/RY/RZ:宽度按最长一行算,高度按三行算
const QString longestAngleLine = QStringLiteral("RX:-360.0");
const int angleTextWidth =
m_viewAngleButton->fontMetrics().horizontalAdvance(longestAngleLine);
const int angleLineHeight = m_viewAngleButton->fontMetrics().lineSpacing();
m_viewAngleButton->setFixedSize(angleTextWidth + 24, angleLineHeight * 3 + 16);
m_viewAngleButton->setVisible(m_showControlButtons);
// ── 右侧悬浮缩放按钮(垂直居中,与 CloudView 位置一致) ──
const QString zoomButtonStyle =
"QToolButton {"
" background-color: rgba(20, 36, 58, 200);"
" color: #e8f2fb;"
" border: 1px solid #2b4f77;"
" border-radius: 4px;"
" font-size: 26px;"
" font-weight: bold;"
"}"
"QToolButton:hover { background-color: #204a72; }"
"QToolButton:pressed { background-color: #10243a; }";
const int zoomButtonSize = 28;
m_zoomInButton = new QToolButton(this);
m_zoomInButton->setText("+");
m_zoomInButton->setFixedSize(zoomButtonSize, zoomButtonSize);
m_zoomInButton->setStyleSheet(zoomButtonStyle);
m_zoomInButton->setAutoRepeat(true);
m_zoomInButton->setAutoRepeatDelay(300);
m_zoomInButton->setAutoRepeatInterval(50);
connect(m_zoomInButton, &QToolButton::clicked, this, &PointCloudGLWidget::zoomIn);
m_zoomInButton->setVisible(m_showControlButtons);
m_zoomOutButton = new QToolButton(this);
m_zoomOutButton->setText("-");
m_zoomOutButton->setFixedSize(zoomButtonSize, zoomButtonSize);
m_zoomOutButton->setStyleSheet(zoomButtonStyle);
m_zoomOutButton->setAutoRepeat(true);
m_zoomOutButton->setAutoRepeatDelay(300);
m_zoomOutButton->setAutoRepeatInterval(50);
connect(m_zoomOutButton, &QToolButton::clicked, this, &PointCloudGLWidget::zoomOut);
m_zoomOutButton->setVisible(m_showControlButtons);
updateViewAngleDisplay();
repositionOverlayControls();
}
void PointCloudGLWidget::repositionOverlayControls()
{
const int widgetWidth = width();
const int widgetHeight = height();
if (widgetWidth <= 0 || widgetHeight <= 0)
{
return;
}
// 左侧视图工具栏:垂直居中
if (m_viewToolbar)
{
// 首次显示和布局切换时先刷新内容尺寸,再按实际高度居中。
if (m_viewToolbar->layout())
{
m_viewToolbar->layout()->activate();
}
m_viewToolbar->adjustSize();
m_viewToolbar->move(8, qMax(0, (widgetHeight - m_viewToolbar->height()) / 2));
m_viewToolbar->raise();
}
// 左下角旋转角度按钮
if (m_viewAngleButton)
{
const int margin = 8;
const int x = qMax(0, qMin(margin, widgetWidth - m_viewAngleButton->width()));
const int y = qMax(0, widgetHeight - m_viewAngleButton->height() - margin);
m_viewAngleButton->move(x, y);
m_viewAngleButton->raise();
}
// 右侧缩放按钮:垂直居中
if (m_zoomInButton && m_zoomOutButton)
{
const int buttonWidth = m_zoomInButton->width();
const int buttonHeight = m_zoomInButton->height();
const int margin = 8;
const int gap = 4;
const int x = qMax(0, widgetWidth - buttonWidth - margin);
const int y = qMax(0, (widgetHeight - buttonHeight * 2 - gap) / 2);
m_zoomInButton->move(x, y);
m_zoomOutButton->move(x, y + buttonHeight + gap);
m_zoomInButton->raise();
m_zoomOutButton->raise();
}
}
void PointCloudGLWidget::updateViewAngleDisplay()
{
if (!m_viewAngleButton) {
return;
}
m_viewAngleButton->setText(QStringLiteral("RX:%1\nRY:%2\nRZ:%3")
.arg(m_rotationX, 0, 'f', 1)
.arg(m_rotationY, 0, 'f', 1)
.arg(m_rotationZ, 0, 'f', 1));
}
void PointCloudGLWidget::showViewAngleDialog()
{
QDialog dialog(this);
dialog.setWindowTitle(QStringLiteral("设置视图角度"));
dialog.setMinimumWidth(240);
auto* form = new QFormLayout(&dialog);
auto* spinX = new QDoubleSpinBox(&dialog);
auto* spinY = new QDoubleSpinBox(&dialog);
auto* spinZ = new QDoubleSpinBox(&dialog);
for (QDoubleSpinBox* spin : {spinX, spinY, spinZ}) {
spin->setRange(-180.0, 180.0);
spin->setDecimals(1);
spin->setSingleStep(1.0);
spin->setSuffix(QStringLiteral(" °"));
}
spinX->setValue(m_rotationX);
spinY->setValue(m_rotationY);
spinZ->setValue(m_rotationZ);
form->addRow(QStringLiteral("RX"), spinX);
form->addRow(QStringLiteral("RY"), spinY);
form->addRow(QStringLiteral("RZ"), spinZ);
auto* buttons = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
buttons->button(QDialogButtonBox::Ok)->setText(QStringLiteral("确认"));
buttons->button(QDialogButtonBox::Cancel)->setText(QStringLiteral("取消"));
connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
form->addRow(buttons);
if (dialog.exec() == QDialog::Accepted) {
setViewAngles(static_cast<float>(spinX->value()),
static_cast<float>(spinY->value()),
static_cast<float>(spinZ->value()));
}
}
void PointCloudGLWidget::initializeGL()
{
initializeOpenGLFunctions();
CLOUD_LOG_INFO("[CloudView] OpenGL initialized, version: %s\n", reinterpret_cast<const char*>(glGetString(GL_VERSION)));
glClearColor(0.15f, 0.15f, 0.15f, 1.0f);
glEnable(GL_DEPTH_TEST);
// 桌面 GL(GL 3.2+)下,顶点着色器输出的 gl_PointSize 需显式开启才生效;
// GLES2 总是使用 shader 点大小,无需此开关。
if (QOpenGLContext::currentContext() &&
!QOpenGLContext::currentContext()->isOpenGLES()) {
glEnable(GL_PROGRAM_POINT_SIZE);
}
if (!m_dynamicVbo.isCreated()) {
m_dynamicVbo.create();
}
m_shaderReady = false;
ensureShader();
}
bool PointCloudGLWidget::ensureShader()
{
if (m_shaderReady) {
return m_shader != nullptr;
}
if (!m_shader) {
auto* program = new QOpenGLShaderProgram();
program->bindAttributeLocation("aPosition", 0);
program->bindAttributeLocation("aColor", 1);
if (!program->addShaderFromSourceCode(
QOpenGLShader::Vertex,
// GLSL ES 1.00 的 vertex shader 有默认 float 精度(highp),
// 但显式声明更稳妥,兼容性更好。
"precision highp float;\n"
"attribute vec3 aPosition;\n"
"attribute vec3 aColor;\n"
"uniform mat4 uMvp;\n"
"uniform float uPointSize;\n"
"uniform float uAlpha;\n"
"uniform float uUseVertexColor;\n"
"uniform vec3 uColor;\n"
"varying vec3 vColor;\n"
"varying float vAlpha;\n"
"void main() {\n"
" gl_Position = uMvp * vec4(aPosition, 1.0);\n"
" gl_PointSize = uPointSize;\n"
" vColor = (uUseVertexColor > 0.5) ? aColor : uColor;\n"
" vAlpha = uAlpha;\n"
"}\n") ||
!program->addShaderFromSourceCode(
QOpenGLShader::Fragment,
// GLSL ES 1.00 的 fragment shader 没有默认 float 精度,
// 必须显式声明,否则 Mali 等 GLES 驱动编译失败(S0032)。
"precision highp float;\n"
"varying vec3 vColor;\n"
"varying float vAlpha;\n"
"void main() {\n"
" gl_FragColor = vec4(vColor, vAlpha);\n"
"}\n")) {
delete program;
m_shaderReady = false;
return false;
}
if (!program->link()) {
delete program;
m_shaderReady = false;
return false;
}
m_shader = program;
m_positionLocation = m_shader->attributeLocation("aPosition");
m_colorLocation = m_shader->attributeLocation("aColor");
m_mvpLocation = m_shader->uniformLocation("uMvp");
m_pointSizeLocation = m_shader->uniformLocation("uPointSize");
m_alphaLocation = m_shader->uniformLocation("uAlpha");
m_useVertexColorLocation = m_shader->uniformLocation("uUseVertexColor");
m_uniformColorLocation = m_shader->uniformLocation("uColor");
}
m_shaderReady = m_shader != nullptr;
return m_shaderReady;
}
QVector3D PointCloudGLWidget::colorForIndex(int colorIndex) const
{
static const QVector3D colorTable[COLOR_COUNT] = {
QVector3D(0.75f, 0.75f, 0.75f), // 浅灰
QVector3D(0.3f, 1.0f, 0.3f), // 浅绿
QVector3D(0.3f, 0.3f, 1.0f), // 浅蓝
QVector3D(1.0f, 1.0f, 0.3f), // 黄
QVector3D(0.3f, 1.0f, 1.0f), // 青
QVector3D(1.0f, 0.3f, 1.0f), // 品红
QVector3D(1.0f, 1.0f, 1.0f) // 白
};
return colorTable[colorIndex % COLOR_COUNT];
}
void PointCloudGLWidget::drawCloud(PointCloudData& data, float pointSize)
{
if (data.vertices.empty() || !m_shader) {
return;
}
m_shader->bind();
glUniformMatrix4fv(m_mvpLocation, 1, GL_FALSE, m_mvp.constData());
glUniform1f(m_pointSizeLocation, pointSize);
glUniform1f(m_alphaLocation, 1.0f);
// 部分驱动忽略 shader 里的 gl_PointSize,固定管线点大小作为兜底。
glPointSize(pointSize);
data.vertexBuffer.bind();
glEnableVertexAttribArray(m_positionLocation);
glVertexAttribPointer(m_positionLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
const bool useVertexColor = (data.hasColor || data.pseudoColor) &&
data.colorBuffer.isCreated() && !data.colors.empty();
if (useVertexColor) {
data.colorBuffer.bind();
glEnableVertexAttribArray(m_colorLocation);
glVertexAttribPointer(m_colorLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
glUniform1f(m_useVertexColorLocation, 1.0f);
} else {
glDisableVertexAttribArray(m_colorLocation);
const QVector3D color = colorForIndex(data.colorIndex);
glUniform3f(m_uniformColorLocation, color.x(), color.y(), color.z());
glUniform1f(m_useVertexColorLocation, 0.0f);
}
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(data.vertices.size() / 3));
glDisableVertexAttribArray(m_positionLocation);
if (useVertexColor) {
glDisableVertexAttribArray(m_colorLocation);
}
data.vertexBuffer.release();
data.colorBuffer.release();
}
void PointCloudGLWidget::drawPrimitives(const QMatrix4x4& mvp,
const float* positions,
int vertexCount,
GLenum mode,
const float* colors,
const QVector3D& uniformColor,
float pointSize,
float alpha,
bool depthTest)
{
if (!positions || vertexCount <= 0 || !m_shader) {
return;
}
m_shader->bind();
glUniformMatrix4fv(m_mvpLocation, 1, GL_FALSE, mvp.constData());
glUniform1f(m_pointSizeLocation, pointSize);
glUniform1f(m_alphaLocation, alpha);
if (!m_dynamicVbo.isCreated())
{
m_dynamicVbo.create();
}
m_dynamicVbo.setUsagePattern(QOpenGLBuffer::DynamicDraw);
if (depthTest) {
glEnable(GL_DEPTH_TEST);
} else {
glDisable(GL_DEPTH_TEST);
}
const GLsizeiptr positionBytes =
static_cast<GLsizeiptr>(vertexCount) * 3 * sizeof(float);
const GLsizeiptr colorBytes =
colors ? static_cast<GLsizeiptr>(vertexCount) * 3 * sizeof(float) : 0;
m_dynamicVbo.bind();
m_dynamicVbo.allocate(positionBytes + colorBytes);
m_dynamicVbo.write(0, positions, positionBytes);
if (colors) {
m_dynamicVbo.write(positionBytes, colors, colorBytes);
}
glEnableVertexAttribArray(m_positionLocation);
glVertexAttribPointer(m_positionLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
if (colors) {
glEnableVertexAttribArray(m_colorLocation);
glVertexAttribPointer(m_colorLocation, 3, GL_FLOAT, GL_FALSE, 0,
reinterpret_cast<void*>(positionBytes));
glUniform1f(m_useVertexColorLocation, 1.0f);
} else {
glDisableVertexAttribArray(m_colorLocation);
glUniform3f(m_uniformColorLocation,
uniformColor.x(), uniformColor.y(), uniformColor.z());
glUniform1f(m_useVertexColorLocation, 0.0f);
}
glDrawArrays(mode, 0, vertexCount);
m_dynamicVbo.release();
glDisableVertexAttribArray(m_positionLocation);
if (colors) {
glDisableVertexAttribArray(m_colorLocation);
}
glEnable(GL_DEPTH_TEST);
}
void PointCloudGLWidget::resetRenderState()
{
glDisableVertexAttribArray(m_positionLocation);
glDisableVertexAttribArray(m_colorLocation);
m_dynamicVbo.release();
if (m_shader) {
m_shader->release();
}
glEnable(GL_DEPTH_TEST);
}
void PointCloudGLWidget::resizeGL(int w, int h)
{
glViewport(0, 0, w, h);
m_projection.setToIdentity();
float aspect = static_cast<float>(w) / static_cast<float>(h > 0 ? h : 1);
// 放大裁剪范围以容纳激光雷达点云(最大可能跨越数十万 mm)
// near 取 1.0 保留 depth buffer 精度,far 放到 1e7 避免远点被裁掉
m_projection.perspective(45.0f, aspect, 1.0f, 1.0e7f);
}
void PointCloudGLWidget::resizeEvent(QResizeEvent* event)
{
QOpenGLWidget::resizeEvent(event);
// 覆盖控件依赖视口尺寸,父布局变化后必须重新计算位置。
repositionOverlayControls();
updateControlButtonGeometry();
}
void PointCloudGLWidget::paintGL()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
if (!ensureShader()) {
return;
}
m_view.setToIdentity();
// 平移在相机空间中进行,使鼠标拖动方向与屏幕方向一致
m_view.translate(-m_pan.x(), -m_pan.y(), -m_distance);
// 使用四元数旋转(物体坐标系旋转)
m_view.rotate(m_rotation);
m_view.translate(-m_center);
QMatrix4x4 modelView = m_view * m_model;
updateProjection(modelView);
m_mvp = m_projection * modelView;
const float pointSize = effectivePointSize();
for (size_t cloudIdx = 0; cloudIdx < m_pointClouds.size(); ++cloudIdx) {
auto& cloudData = m_pointClouds[cloudIdx];
if (cloudData.vertices.empty()) {
continue;
}
// 确保 VBO 已创建(延迟上传,因为 addPointCloud 可能在 GL 上下文外调用)
if (!cloudData.vboCreated) {
uploadToVBO(cloudData);
}
drawCloud(cloudData, pointSize);
// 绘制自定义大小的点(RGBA 中 A > 1 的点)
if (cloudData.hasCustomPointSizes) {
glDepthFunc(GL_LEQUAL); // 允许在相同深度覆盖绘制
const bool cloudHasColor = (cloudData.hasColor || cloudData.pseudoColor) &&
cloudData.colorBuffer.isCreated() &&
!cloudData.colors.empty();
for (const auto& group : cloudData.customPointSizeGroups) {
QVector<float> positions;
positions.reserve(static_cast<int>(group.indices.size()) * 3);
QVector<float> colors;
if (cloudHasColor) {
colors.reserve(static_cast<int>(group.indices.size()) * 3);
}
for (size_t idx : group.indices) {
const size_t vi = idx * 3;
if (vi + 2 < cloudData.vertices.size()) {
positions.append(cloudData.vertices[vi]);
positions.append(cloudData.vertices[vi + 1]);
positions.append(cloudData.vertices[vi + 2]);
if (cloudHasColor && vi + 2 < cloudData.colors.size()) {
colors.append(cloudData.colors[vi]);
colors.append(cloudData.colors[vi + 1]);
colors.append(cloudData.colors[vi + 2]);
}
}
}
if (!positions.isEmpty()) {
drawPrimitives(m_mvp,
positions.constData(),
positions.size() / 3,
GL_POINTS,
cloudHasColor ? colors.constData() : nullptr,
colorForIndex(cloudData.colorIndex),
group.pointSize,
1.0f,
true);
}
}
glDepthFunc(GL_LESS); // 恢复默认深度测试
}
}
drawSelectedPoints();
drawMeasurementLine();
drawSelectedLine();
drawLineSegments();
drawBasicShapeSurfaces();
drawBasicShapeSegments();
drawBasicShapePoints();
drawPosePoints();
// 最后绘制坐标系指示器(覆盖在所有内容之上)
drawAxis();
// 清理 GL 状态,避免影响 QPainter 标注
resetRenderState();
// 使用 QPainter 绘制坐标轴 XYZ 标注
drawAxisLabels();
drawSelectionInfoOverlay();
}
void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString& name, bool resetViewAfterAdd)
{
addPointCloudInternal(cloud, name, resetViewAfterAdd, true);
}
void PointCloudGLWidget::addPointCloudInternal(const PointCloudXYZ& cloud, const QString& name, bool resetViewAfterAdd, bool refreshDisplay)
{
if (cloud.empty()) {
CLOUD_LOG_WARN("[CloudView] Cloud is empty, returning\n");
return;
}
PointCloudData data;
data.name = name;
data.hasColor = false;
data.hasLineInfo = !cloud.lineIndices.empty();
data.colorIndex = m_colorIndex; // 分配颜色索引
m_colorIndex = (m_colorIndex + 1) % COLOR_COUNT; // 轮换到下一个颜色
data.vertices.reserve(cloud.size() * 3);
data.totalLines = 0;
data.pointsPerLine = 0;
const float EPSILON = 1e-6f;
int prevLineIdx = -1;
int ptInLineCounter = 0;
int maxPointCount = 0;
for (size_t i = 0; i < cloud.points.size(); ++i) {
// 跟踪线内索引(对所有点递增,包括被过滤的零点)
int lineIdx = -1;
if (data.hasLineInfo && i < cloud.lineIndices.size()) {
lineIdx = cloud.lineIndices[i];
}
if (lineIdx != prevLineIdx) {
ptInLineCounter = 0;
prevLineIdx = lineIdx;
}
int pointInLineIndex = (i < cloud.pointInLineIndices.size())
? cloud.pointInLineIndices[i] : -1;
if (pointInLineIndex < 0) {
pointInLineIndex = ptInLineCounter;
}
maxPointCount = qMax(maxPointCount, pointInLineIndex + 1);
const auto& pt = cloud.points[i];
if (!std::isfinite(pt.x) || !std::isfinite(pt.y) || !std::isfinite(pt.z)) {
ptInLineCounter++;
continue;
}
// 显示时过滤 (0,0,0) 点
if (std::fabs(pt.x) < EPSILON && std::fabs(pt.y) < EPSILON && std::fabs(pt.z) < EPSILON) {
ptInLineCounter++;
continue;
}
data.vertices.push_back(pt.x);
data.vertices.push_back(pt.y);
data.vertices.push_back(pt.z);
expandCloudBounds(data, pt.x, pt.y, pt.z);
// 保存原始索引
data.originalIndices.push_back(static_cast<int>(i));
// 保存线内索引(点在所属线中的位置)
data.pointInLineIndices.push_back(pointInLineIndex);
// 线索引数组必须和顶点数组保持一一对应;未知线号用 -1 占位。
if (data.hasLineInfo) {
data.lineIndices.push_back(lineIdx);
if (lineIdx >= 0 && lineIdx + 1 > data.totalLines) {
data.totalLines = lineIdx + 1;
}
}
ptInLineCounter++;
}
// 计算每线点数(假设网格化点云)
if (data.totalLines > 0) {
data.pointsPerLine = maxPointCount;
}
// 新建纯 XYZ 点云时直接生成颜色,避免随后再次上传同一份 VBO。
rebuildPseudoColors(data, 0);
m_pointClouds.push_back(std::move(data));
// VBO 推迟到 paintGL,避免高频 add 时反复 makeCurrent。
if (!refreshDisplay)
{
return;
}
if (resetViewAfterAdd) {
computeBoundingBox();
resetView();
} else {
computeBoundingBox();
const float fittedDistance = fittedCameraDistance();
if (std::isfinite(fittedDistance)) {
m_distance = qMax(m_distance, fittedDistance);
}
}
update();
}
void PointCloudGLWidget::setControlButtonsVisible(bool visible)
{
m_showControlButtons = visible;
if (m_controlPanel) {
m_controlPanel->setVisible(visible);
}
if (m_viewToolbar) {
m_viewToolbar->setVisible(visible);
}
if (m_viewAngleButton) {
m_viewAngleButton->setVisible(visible);
}
if (m_zoomInButton) {
m_zoomInButton->setVisible(visible);
}
if (m_zoomOutButton) {
m_zoomOutButton->setVisible(visible);
}
if (visible)
{
repositionOverlayControls();
updateControlButtonGeometry();
}
}
void PointCloudGLWidget::createControlButtons()
{
m_controlPanel = new QWidget(this);
m_controlPanel->setObjectName("cloudView3DControlPanel");
m_controlPanel->setStyleSheet(
"QWidget#cloudView3DControlPanel {"
" background-color: rgba(40, 40, 40, 185);"
" border: 1px solid rgba(130, 130, 130, 150);"
" border-radius: 4px;"
"}"
"QToolButton {"
" background-color: rgba(70, 70, 70, 210);"
" color: white;"
" border: 1px solid rgba(150, 150, 150, 120);"
" border-radius: 3px;"
" font-size: 10px;"
"}"
"QToolButton:hover { background-color: rgba(100, 100, 100, 230); }"
"QToolButton:pressed { background-color: rgba(25, 25, 25, 240); }");
QVBoxLayout* panelLayout = new QVBoxLayout(m_controlPanel);
panelLayout->setContentsMargins(4, 4, 4, 4);
panelLayout->setSpacing(3);
auto makeRow = [this, panelLayout]() {
QHBoxLayout* row = new QHBoxLayout();
row->setContentsMargins(0, 0, 0, 0);
row->setSpacing(2);
panelLayout->addLayout(row);
return row;
};
auto addButton = [this](QHBoxLayout* row, const QString& text, const QString& tooltip) {
QToolButton* button = new QToolButton(m_controlPanel);
button->setText(text);
button->setToolTip(tooltip);
button->setFixedSize(26, 24);
button->setAutoRaise(false);
row->addWidget(button);
return button;
};
struct ViewPreset {
const char* text;
const char* tooltip;
float rotX;
float rotY;
float rotZ;
};
const ViewPreset presets[] = {
{"F", "正视", 180.0f, 0.0f, 0.0f},
{"B", "后视", 180.0f, 180.0f, 0.0f},
{"L", "左视", 180.0f, 90.0f, 0.0f},
{"R", "右视", 180.0f, -90.0f, 0.0f},
{"T", "俯视", 90.0f, 0.0f, 0.0f},
{"D", "仰视", -90.0f, 0.0f, 0.0f},
{"A", "机械臂视角", -90.0f, 90.0f, 0.0f}
};
QHBoxLayout* viewRow = makeRow();
for (const ViewPreset& preset : presets) {
QToolButton* button = addButton(viewRow, preset.text, preset.tooltip);
connect(button, &QToolButton::clicked, this, [this, preset]() {
setViewAngles(preset.rotX, preset.rotY, preset.rotZ);
});
}
QHBoxLayout* rotateRow = makeRow();
auto addRotationButton = [this, &addButton, rotateRow](const QString& text,
const QString& tooltip,
float deltaX,
float deltaY,
float deltaZ) {
QToolButton* button = addButton(rotateRow, text, tooltip);
connect(button, &QToolButton::clicked, this, [this, deltaX, deltaY, deltaZ]() {
setViewAngles(m_rotationX + deltaX,
m_rotationY + deltaY,
m_rotationZ + deltaZ);
});
};
addRotationButton("X-", "绕 X 轴反向旋转 5 度", -5.0f, 0.0f, 0.0f);
addRotationButton("X+", "绕 X 轴正向旋转 5 度", 5.0f, 0.0f, 0.0f);
addRotationButton("Y-", "绕 Y 轴反向旋转 5 度", 0.0f, -5.0f, 0.0f);
addRotationButton("Y+", "绕 Y 轴正向旋转 5 度", 0.0f, 5.0f, 0.0f);
addRotationButton("Z-", "绕 Z 轴反向旋转 5 度", 0.0f, 0.0f, -5.0f);
addRotationButton("Z+", "绕 Z 轴正向旋转 5 度", 0.0f, 0.0f, 5.0f);
QToolButton* resetButton = addButton(rotateRow, "0", "重置视图");
connect(resetButton, &QToolButton::clicked, this, &PointCloudGLWidget::resetView);
QHBoxLayout* zoomRow = makeRow();
m_zoomInButton = addButton(zoomRow, "+", "放大");
m_zoomOutButton = addButton(zoomRow, "-", "缩小");
m_zoomInButton->setAutoRepeat(true);
m_zoomOutButton->setAutoRepeat(true);
connect(m_zoomInButton, &QToolButton::clicked, this, &PointCloudGLWidget::zoomIn);
connect(m_zoomOutButton, &QToolButton::clicked, this, &PointCloudGLWidget::zoomOut);
zoomRow->addStretch();
m_controlPanel->adjustSize();
m_controlPanel->setVisible(m_showControlButtons);
updateControlButtonGeometry();
}
void PointCloudGLWidget::updateControlButtonGeometry()
{
if (!m_controlPanel) {
return;
}
m_controlPanel->move(8, 8);
m_controlPanel->raise();
}
void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QString& name, bool resetViewAfterAdd)
{
if (cloud.empty()) {
return;
}
PointCloudData data;
data.name = name;
data.hasColor = true;
data.hasLineInfo = !cloud.lineIndices.empty();
data.colorIndex = m_colorIndex; // 即使有颜色也分配索引(备用)
m_colorIndex = (m_colorIndex + 1) % COLOR_COUNT;
data.vertices.reserve(cloud.size() * 3);
data.colors.reserve(cloud.size() * 3);
data.totalLines = 0;
data.pointsPerLine = 0;
data.hasCustomPointSizes = false;
// 用于按点大小分组的临时 map
std::map<float, std::vector<size_t>> sizeGroupMap;
const float EPSILON = 1e-6f;
int prevLineIdx = -1;
int ptInLineCounter = 0;
int maxPointCount = 0;
for (size_t i = 0; i < cloud.points.size(); ++i) {
const auto& pt = cloud.points[i];
// 跟踪线内索引(对所有点递增,包括被过滤的零点)
int lineIdx = -1;
if (data.hasLineInfo && i < cloud.lineIndices.size()) {
lineIdx = cloud.lineIndices[i];
}
if (lineIdx != prevLineIdx) {
ptInLineCounter = 0;
prevLineIdx = lineIdx;
}
int pointInLineIndex = (i < cloud.pointInLineIndices.size())
? cloud.pointInLineIndices[i] : -1;
if (pointInLineIndex < 0) {
pointInLineIndex = ptInLineCounter;
}
maxPointCount = qMax(maxPointCount, pointInLineIndex + 1);
if (!std::isfinite(pt.x) || !std::isfinite(pt.y) || !std::isfinite(pt.z)) {
ptInLineCounter++;
continue;
}
// 显示时过滤 (0,0,0) 点
if (std::fabs(pt.x) < EPSILON && std::fabs(pt.y) < EPSILON && std::fabs(pt.z) < EPSILON) {
ptInLineCounter++;
continue;
}
size_t pointIndex = data.vertices.size() / 3;
data.vertices.push_back(pt.x);
data.vertices.push_back(pt.y);
data.vertices.push_back(pt.z);
expandCloudBounds(data, pt.x, pt.y, pt.z);
data.colors.push_back(pt.r / 255.0f);
data.colors.push_back(pt.g / 255.0f);
data.colors.push_back(pt.b / 255.0f);
// 保存原始索引
data.originalIndices.push_back(static_cast<int>(i));
// 保存线内索引(点在所属线中的位置)
data.pointInLineIndices.push_back(pointInLineIndex);
// 线索引数组必须和顶点数组保持一一对应;未知线号用 -1 占位。
if (data.hasLineInfo) {
data.lineIndices.push_back(lineIdx);
if (lineIdx >= 0 && lineIdx + 1 > data.totalLines) {
data.totalLines = lineIdx + 1;
}
}
// 收集自定义点大小(A > 1)
if (pt.pointSize > 1.0f) {
sizeGroupMap[pt.pointSize].push_back(pointIndex);
}
ptInLineCounter++;
}
// 构建自定义点大小分组
if (!sizeGroupMap.empty()) {
data.hasCustomPointSizes = true;
for (auto& pair : sizeGroupMap) {
PointCloudData::PointSizeGroup group;
group.pointSize = pair.first;
group.indices = std::move(pair.second);
data.customPointSizeGroups.push_back(std::move(group));
}
CLOUD_LOG_INFO("[CloudView] Found %zu custom point size groups\n", data.customPointSizeGroups.size());
}
// 计算每线点数
if (data.totalLines > 0) {
data.pointsPerLine = maxPointCount;
}
m_pointClouds.push_back(std::move(data));
// VBO 推迟到 paintGL,避免高频 add 时反复 makeCurrent。
computeBoundingBox();
if (resetViewAfterAdd) {
resetView();
} else {
// A line-scan cloud grows along X over time. Keep the user's view
// orientation/pan, but expand the camera distance when the retained
// cloud no longer fits in the frustum.
const float fittedDistance = fittedCameraDistance();
if (std::isfinite(fittedDistance)) {
m_distance = qMax(m_distance, fittedDistance);
}
}
update();
}
void PointCloudGLWidget::appendPointCloud(const PointCloudXYZRGB& cloud,
const QString& name)
{
if (cloud.empty()) {
return;
}
if (m_pointClouds.empty() || !m_pointClouds.back().hasColor) {
addPointCloud(cloud, name);
return;
}
PointCloudData& data = m_pointClouds.back();
if (!name.isEmpty()) {
data.name = name;
}
const bool incomingHasLineInfo = !cloud.lineIndices.empty();
if (incomingHasLineInfo && !data.hasLineInfo) {
// 后续数据首次带线信息时,为已有顶点补齐未知线号,避免索引数组错位。
data.lineIndices.resize(data.vertices.size() / 3, -1);
data.hasLineInfo = true;
} else {
data.hasLineInfo = data.hasLineInfo || incomingHasLineInfo;
}
const size_t originalIndexOffset = data.vertices.size() / 3;
const float epsilon = 1e-6f;
int previousLineIndex = -1;
int pointInLineIndex = 0;
int maxPointCount = data.pointsPerLine;
for (size_t index = 0; index < cloud.points.size(); ++index) {
const Point3DRGB& point = cloud.points[index];
int lineIndex = -1;
if (incomingHasLineInfo && index < cloud.lineIndices.size()) {
lineIndex = cloud.lineIndices[index];
}
if (lineIndex != previousLineIndex) {
pointInLineIndex = 0;
previousLineIndex = lineIndex;
}
int sourcePointIndex = (index < cloud.pointInLineIndices.size())
? cloud.pointInLineIndices[index] : -1;
if (sourcePointIndex < 0) {
sourcePointIndex = pointInLineIndex;
}
maxPointCount = qMax(maxPointCount, sourcePointIndex + 1);
if (!std::isfinite(point.x) || !std::isfinite(point.y) ||
!std::isfinite(point.z) ||
(std::fabs(point.x) < epsilon &&
std::fabs(point.y) < epsilon &&
std::fabs(point.z) < epsilon)) {
++pointInLineIndex;
continue;
}
const size_t pointIndex = data.vertices.size() / 3;
data.vertices.push_back(point.x);
data.vertices.push_back(point.y);
data.vertices.push_back(point.z);
expandCloudBounds(data, point.x, point.y, point.z);
data.colors.push_back(point.r / 255.0f);
data.colors.push_back(point.g / 255.0f);
data.colors.push_back(point.b / 255.0f);
data.originalIndices.push_back(
static_cast<int>(originalIndexOffset + index));
data.pointInLineIndices.push_back(sourcePointIndex);
if (data.hasLineInfo) {
data.lineIndices.push_back(lineIndex);
if (lineIndex >= 0) {
data.totalLines = qMax(data.totalLines, lineIndex + 1);
}
}
if (point.pointSize > 1.0f) {
auto sizeGroup = std::find_if(
data.customPointSizeGroups.begin(),
data.customPointSizeGroups.end(),
[&point](const PointCloudData::PointSizeGroup& group) {
return group.pointSize == point.pointSize;
});
if (sizeGroup == data.customPointSizeGroups.end()) {
PointCloudData::PointSizeGroup group;
group.pointSize = point.pointSize;
data.customPointSizeGroups.push_back(std::move(group));
sizeGroup = data.customPointSizeGroups.end() - 1;
}
sizeGroup->indices.push_back(pointIndex);
data.hasCustomPointSizes = true;
}
++pointInLineIndex;
}
if (data.totalLines > 0) {
data.pointsPerLine = maxPointCount;
}
makeCurrent();
uploadToVBO(data);
doneCurrent();
computeBoundingBox();
const float fittedDistance = fittedCameraDistance();
if (std::isfinite(fittedDistance)) {
m_distance = qMax(m_distance, fittedDistance);
}
update();
}
void PointCloudGLWidget::appendPointCloud(const PointCloudXYZ& cloud, const QString& name)
{
if (cloud.empty()) {
return;
}
if (m_pointClouds.empty() || m_pointClouds.back().hasColor) {
addPointCloud(cloud, name, false);
return;
}
PointCloudData& data = m_pointClouds.back();
if (!name.isEmpty()) {
data.name = name;
}
const bool incomingHasLineInfo = !cloud.lineIndices.empty();
if (incomingHasLineInfo && !data.hasLineInfo) {
// 后续数据首次带线信息时,为已有顶点补齐未知线号,避免索引数组错位。
data.lineIndices.resize(data.vertices.size() / 3, -1);
data.hasLineInfo = true;
} else {
data.hasLineInfo = data.hasLineInfo || incomingHasLineInfo;
}
const size_t originalIndexOffset = data.vertices.size() / 3;
const size_t firstNewVertex = originalIndexOffset;
const float epsilon = 1e-6f;
int previousLineIndex = -1;
int pointInLineIndex = 0;
int maxPointCount = data.pointsPerLine;
for (size_t index = 0; index < cloud.points.size(); ++index) {
const Point3D& point = cloud.points[index];
int lineIndex = -1;
if (incomingHasLineInfo && index < cloud.lineIndices.size()) {
lineIndex = cloud.lineIndices[index];
}
if (lineIndex != previousLineIndex) {
pointInLineIndex = 0;
previousLineIndex = lineIndex;
}
int sourcePointIndex = (index < cloud.pointInLineIndices.size())
? cloud.pointInLineIndices[index] : -1;
if (sourcePointIndex < 0) {
sourcePointIndex = pointInLineIndex;
}
maxPointCount = qMax(maxPointCount, sourcePointIndex + 1);
if (!std::isfinite(point.x) || !std::isfinite(point.y) ||
!std::isfinite(point.z) ||
(std::fabs(point.x) < epsilon &&
std::fabs(point.y) < epsilon &&
std::fabs(point.z) < epsilon)) {
++pointInLineIndex;
continue;
}
data.vertices.push_back(point.x);
data.vertices.push_back(point.y);
data.vertices.push_back(point.z);
expandCloudBounds(data, point.x, point.y, point.z);
data.originalIndices.push_back(static_cast<int>(originalIndexOffset + index));
data.pointInLineIndices.push_back(sourcePointIndex);
if (data.hasLineInfo) {
data.lineIndices.push_back(lineIndex);
if (lineIndex >= 0) {
data.totalLines = qMax(data.totalLines, lineIndex + 1);
}
}
++pointInLineIndex;
}
if (data.totalLines > 0) {
// 过滤后各扫描线可能不等长,使用原始线内索引的最大值保留网格位置。
data.pointsPerLine = maxPointCount;
}
// 流式追加时只为新增顶点生成伪彩,避免重复扫描历史点云。
rebuildPseudoColors(data, firstNewVertex);
makeCurrent();
uploadToVBO(data);
doneCurrent();
computeBoundingBox();
m_distance = qMax(m_distance, fittedCameraDistance());
update();
}
void PointCloudGLWidget::setNewestPointCloudLineLayout(int totalLines, int pointsPerLine)
{
if (m_pointClouds.empty())
{
return;
}
PointCloudData& data = m_pointClouds.back();
if (totalLines > 0)
{
data.hasLineInfo = true;
data.totalLines = qMax(data.totalLines, totalLines);
}
if (pointsPerLine > 0)
{
data.pointsPerLine = qMax(data.pointsPerLine, pointsPerLine);
}
}
void PointCloudGLWidget::clearPointClouds()
{
clearPointCloudBatches();
m_streamDisplayedLineCount = 0;
// 释放所有 VBO
makeCurrent();
for (auto& cloudData : m_pointClouds) {
releaseVBO(cloudData);
}
doneCurrent();
m_pointClouds.clear();
m_selectedPoints.clear();
m_selectedLine = SelectedLineInfo();
m_lineSegments.clear();
m_basicShapeSegments.clear();
m_basicShapeSurfaces.clear();
m_basicShapePoints.clear();
m_posePoints.clear();
m_minBound = QVector3D(-50, -50, -50);
m_maxBound = QVector3D(50, 50, 50);
m_center = QVector3D(0, 0, 0);
m_colorIndex = 0; // 重置颜色索引
resetView();
update();
}
bool PointCloudGLWidget::removeOldestPointCloud(bool refreshDisplay)
{
if (m_pointClouds.empty()) {
return false;
}
const int removedStreamLines = m_pointClouds.front().streamBatch
? m_pointClouds.front().sourceLineCount
: 0;
if (m_pointClouds.front().vboCreated) {
makeCurrent();
releaseVBO(m_pointClouds.front());
doneCurrent();
}
m_pointClouds.pop_front();
m_streamDisplayedLineCount =
qMax(0, m_streamDisplayedLineCount - removedStreamLines);
// Cloud indices change after removing the first item, so stale selection
// information must not be kept.
m_selectedPoints.clear();
m_selectedLine = SelectedLineInfo();
// 伪彩按绝对 Z 周期着色,删块不必重算其余点云颜色/VBO。
if (refreshDisplay) {
computeBoundingBox();
update();
}
return true;
}
bool PointCloudGLWidget::removeNewestPointCloud(bool refreshDisplay)
{
if (m_pointClouds.empty()) {
return false;
}
const int removedStreamLines = m_pointClouds.back().streamBatch
? m_pointClouds.back().sourceLineCount
: 0;
if (m_pointClouds.back().vboCreated) {
makeCurrent();
releaseVBO(m_pointClouds.back());
doneCurrent();
}
m_pointClouds.pop_back();
m_streamDisplayedLineCount =
qMax(0, m_streamDisplayedLineCount - removedStreamLines);
m_selectedPoints.clear();
m_selectedLine = SelectedLineInfo();
if (refreshDisplay) {
computeBoundingBox();
update();
}
return true;
}
void PointCloudGLWidget::setStreamRefreshRate(int refreshRateHz)
{
m_streamRefreshRate = qBound(1, refreshRateHz, 1000);
const int intervalMs =
qMax(1, qRound(1000.0 / static_cast<double>(m_streamRefreshRate)));
m_streamRefreshTimer->setInterval(intervalMs);
}
void PointCloudGLWidget::setStreamLineLimit(int maxLines)
{
m_streamLineLimit = qMax(1, maxLines);
trimQueuedPointCloudBatches();
trimDisplayedStreamBatches(true);
}
void PointCloudGLWidget::startStreamRefresh()
{
if (!m_streamRefreshTimer->isActive())
{
m_streamRefreshTimer->start();
}
}
void PointCloudGLWidget::stopStreamRefresh(bool flushPending)
{
m_streamRefreshTimer->stop();
if (flushPending)
{
flushPointCloudBatches();
}
}
void PointCloudGLWidget::queuePointCloudBatch(PointCloudXYZ cloud, int sourceLineCount, const QString& name)
{
if (cloud.empty() || sourceLineCount <= 0)
{
return;
}
// 流式批次使用局部连续线号;缺失线号时只能安全地按单线处理。
if (cloud.lineIndices.size() != cloud.points.size())
{
if (sourceLineCount != 1)
{
CLOUD_LOG_WARN("[CloudView] Stream batch has no complete line index map; treating it as one line\n");
}
sourceLineCount = 1;
cloud.lineIndices.assign(cloud.points.size(), 0);
}
else
{
for (int& lineIndex : cloud.lineIndices)
{
lineIndex = qBound(0, lineIndex, sourceLineCount - 1);
}
}
if (cloud.pointInLineIndices.size() != cloud.points.size())
{
cloud.pointInLineIndices.assign(cloud.points.size(), -1);
}
QueuedPointCloudBatch batch;
batch.cloud = std::move(cloud);
batch.name = name;
batch.sourceLineCount = sourceLineCount;
m_queuedPointCloudLineCount += sourceLineCount;
m_queuedPointCloudBatches.push_back(std::move(batch));
// 输入缓存有界,GUI 暂时跟不上时丢最旧扫描线以保持实时性。
trimQueuedPointCloudBatches();
startStreamRefresh();
}
void PointCloudGLWidget::discardLeadingLines(QueuedPointCloudBatch& batch, int lineCount)
{
const int discardCount = qBound(0, lineCount, batch.sourceLineCount);
if (discardCount == 0)
{
return;
}
if (discardCount == batch.sourceLineCount)
{
batch.cloud.clear();
batch.sourceLineCount = 0;
return;
}
size_t writeIndex = 0;
for (size_t readIndex = 0;
readIndex < batch.cloud.points.size();
++readIndex)
{
const int lineIndex = batch.cloud.lineIndices[readIndex];
if (lineIndex < discardCount)
{
continue;
}
if (writeIndex != readIndex)
{
batch.cloud.points[writeIndex] =
std::move(batch.cloud.points[readIndex]);
batch.cloud.pointInLineIndices[writeIndex] =
batch.cloud.pointInLineIndices[readIndex];
}
batch.cloud.lineIndices[writeIndex] = lineIndex - discardCount;
++writeIndex;
}
batch.cloud.points.resize(writeIndex);
batch.cloud.lineIndices.resize(writeIndex);
batch.cloud.pointInLineIndices.resize(writeIndex);
batch.sourceLineCount -= discardCount;
}
void PointCloudGLWidget::trimQueuedPointCloudBatches()
{
while (m_queuedPointCloudLineCount > m_streamLineLimit &&
!m_queuedPointCloudBatches.empty())
{
const int overflow =
m_queuedPointCloudLineCount - m_streamLineLimit;
QueuedPointCloudBatch& oldest = m_queuedPointCloudBatches.front();
if (oldest.sourceLineCount <= overflow)
{
m_queuedPointCloudLineCount -= oldest.sourceLineCount;
m_queuedPointCloudBatches.pop_front();
continue;
}
discardLeadingLines(oldest, overflow);
m_queuedPointCloudLineCount -= overflow;
}
}
void PointCloudGLWidget::flushPointCloudBatches()
{
if (m_queuedPointCloudBatches.empty())
{
return;
}
size_t totalPointCount = 0;
for (const QueuedPointCloudBatch& batch : m_queuedPointCloudBatches)
{
totalPointCount += batch.cloud.size();
}
PointCloudXYZ mergedCloud;
mergedCloud.reserve(totalPointCount);
QString cloudName;
int mergedLineCount = 0;
for (QueuedPointCloudBatch& batch : m_queuedPointCloudBatches)
{
mergedCloud.points.insert(
mergedCloud.points.end(),
std::make_move_iterator(batch.cloud.points.begin()),
std::make_move_iterator(batch.cloud.points.end()));
for (int lineIndex : batch.cloud.lineIndices)
{
mergedCloud.lineIndices.push_back(lineIndex + mergedLineCount);
}
mergedCloud.pointInLineIndices.insert(
mergedCloud.pointInLineIndices.end(),
std::make_move_iterator(batch.cloud.pointInLineIndices.begin()),
std::make_move_iterator(batch.cloud.pointInLineIndices.end()));
if (!batch.name.isEmpty())
{
cloudName = batch.name;
}
mergedLineCount += batch.sourceLineCount;
}
m_queuedPointCloudBatches.clear();
m_queuedPointCloudLineCount = 0;
if (mergedCloud.empty() || mergedLineCount <= 0)
{
return;
}
const bool resetViewAfterAdd = m_pointClouds.empty();
const int maxLinesPerBatch = qMax(1, m_streamLineLimit);
bool addedStreamBatch = false;
// 一次刷新可能包含的线数大于显示窗口;按源线拆成小块,保证淘汰
// 最旧数据时不会把本次唯一的新数据整块删除。
for (int firstLine = 0; firstLine < mergedLineCount;
firstLine += maxLinesPerBatch)
{
const int chunkLineCount =
qMin(maxLinesPerBatch, mergedLineCount - firstLine);
PointCloudXYZ chunk;
chunk.lineIndices.reserve(mergedCloud.points.size());
chunk.pointInLineIndices.reserve(mergedCloud.points.size());
chunk.points.reserve(mergedCloud.points.size());
for (size_t pointIndex = 0;
pointIndex < mergedCloud.points.size(); ++pointIndex)
{
if (pointIndex >= mergedCloud.lineIndices.size())
{
continue;
}
const int lineIndex = mergedCloud.lineIndices[pointIndex];
if (lineIndex < firstLine ||
lineIndex >= firstLine + chunkLineCount)
{
continue;
}
chunk.points.push_back(std::move(mergedCloud.points[pointIndex]));
chunk.lineIndices.push_back(lineIndex - firstLine);
const int pointInLine =
pointIndex < mergedCloud.pointInLineIndices.size()
? mergedCloud.pointInLineIndices[pointIndex]
: -1;
chunk.pointInLineIndices.push_back(pointInLine);
}
if (chunk.empty())
{
continue;
}
const size_t cloudCountBeforeAdd = m_pointClouds.size();
// 33 ms 内的每个源线块只构建一个 CPU 块,并延迟到 paintGL
// 上传 VBO;同一 tick 仍只触发一次最终刷新。
addPointCloudInternal(chunk, cloudName,
resetViewAfterAdd && !addedStreamBatch, false);
if (m_pointClouds.size() == cloudCountBeforeAdd)
{
continue;
}
PointCloudData& displayedBatch = m_pointClouds.back();
displayedBatch.streamBatch = true;
displayedBatch.sourceLineCount = chunkLineCount;
// addPointCloudInternal 只能从有效点推断线数;这里恢复完整的
// 源布局,包含被过滤为空的尾线。
displayedBatch.totalLines = chunkLineCount;
m_streamDisplayedLineCount += chunkLineCount;
trimDisplayedStreamBatches(false);
addedStreamBatch = true;
}
if (!addedStreamBatch)
{
return;
}
computeBoundingBox();
if (resetViewAfterAdd)
{
resetView();
}
else
{
const float fittedDistance = fittedCameraDistance();
if (std::isfinite(fittedDistance))
{
m_distance = qMax(m_distance, fittedDistance);
}
update();
}
}
void PointCloudGLWidget::clearPointCloudBatches()
{
m_queuedPointCloudBatches.clear();
m_queuedPointCloudLineCount = 0;
}
bool PointCloudGLWidget::removeOldestStreamPointCloud(bool refreshDisplay)
{
const auto oldest = std::find_if(
m_pointClouds.begin(), m_pointClouds.end(),
[](const PointCloudData& data)
{
return data.streamBatch;
});
if (oldest == m_pointClouds.end())
{
return false;
}
const int removedLineCount = oldest->sourceLineCount;
// deque 中间删除会移动后续对象;先释放这些 VBO,移动后由 paintGL 重建。
makeCurrent();
for (auto data = oldest; data != m_pointClouds.end(); ++data)
{
releaseVBO(*data);
}
m_pointClouds.erase(oldest);
doneCurrent();
m_streamDisplayedLineCount =
qMax(0, m_streamDisplayedLineCount - removedLineCount);
m_selectedPoints.clear();
m_selectedLine = SelectedLineInfo();
if (refreshDisplay)
{
computeBoundingBox();
update();
}
return true;
}
void PointCloudGLWidget::trimDisplayedStreamBatches(bool refreshDisplay)
{
bool removed = false;
while (m_streamDisplayedLineCount > m_streamLineLimit)
{
const int overflow = m_streamDisplayedLineCount - m_streamLineLimit;
auto oldest = std::find_if(
m_pointClouds.begin(), m_pointClouds.end(),
[](const PointCloudData& data)
{
return data.streamBatch;
});
if (oldest != m_pointClouds.end() &&
oldest->sourceLineCount > overflow &&
(oldest->vertices.empty() ||
(oldest->hasLineInfo &&
oldest->lineIndices.size() == oldest->vertices.size() / 3)))
{
// 当前批次可能包含整个刷新周期;只裁掉其前缀,保留最新扫描线。
makeCurrent();
releaseVBO(*oldest);
discardLeadingStreamLines(*oldest, overflow);
doneCurrent();
m_streamDisplayedLineCount -= overflow;
// 前缀裁剪后顶点索引已重排,旧选择不能继续指向当前数据。
m_selectedPoints.clear();
m_selectedLine = SelectedLineInfo();
removed = true;
break;
}
if (!removeOldestStreamPointCloud(false))
{
m_streamDisplayedLineCount = 0;
break;
}
removed = true;
}
if (removed && refreshDisplay)
{
computeBoundingBox();
update();
}
}
void PointCloudGLWidget::discardLeadingStreamLines(PointCloudData& data,
int lineCount)
{
const int discardCount = qBound(0, lineCount, data.sourceLineCount);
const size_t oldVertexCount = data.vertices.size() / 3;
if (discardCount == 0)
{
return;
}
if (discardCount == data.sourceLineCount)
{
data.vertices.clear();
data.colors.clear();
data.lineIndices.clear();
data.originalIndices.clear();
data.pointInLineIndices.clear();
data.customPointSizeGroups.clear();
data.hasCustomPointSizes = false;
data.sourceLineCount = 0;
data.totalLines = 0;
data.hasBounds = false;
return;
}
if (oldVertexCount == 0)
{
data.sourceLineCount -= discardCount;
data.totalLines = qMax(0, data.totalLines - discardCount);
return;
}
if (!data.hasLineInfo || data.lineIndices.size() != oldVertexCount)
{
return;
}
std::vector<float> vertices;
std::vector<float> colors;
std::vector<int> lineIndices;
std::vector<int> originalIndices;
std::vector<int> pointInLineIndices;
std::vector<int> remappedIndices(oldVertexCount, -1);
vertices.reserve(data.vertices.size());
if (data.colors.size() == data.vertices.size())
{
colors.reserve(data.colors.size());
}
lineIndices.reserve(oldVertexCount);
originalIndices.reserve(data.originalIndices.size());
pointInLineIndices.reserve(data.pointInLineIndices.size());
for (size_t oldIndex = 0; oldIndex < oldVertexCount; ++oldIndex)
{
const int oldLineIndex = data.lineIndices[oldIndex];
if (oldLineIndex < discardCount)
{
continue;
}
const size_t newIndex = vertices.size() / 3;
remappedIndices[oldIndex] = static_cast<int>(newIndex);
const size_t vertexOffset = oldIndex * 3;
vertices.push_back(data.vertices[vertexOffset]);
vertices.push_back(data.vertices[vertexOffset + 1]);
vertices.push_back(data.vertices[vertexOffset + 2]);
if (data.colors.size() == data.vertices.size())
{
colors.push_back(data.colors[vertexOffset]);
colors.push_back(data.colors[vertexOffset + 1]);
colors.push_back(data.colors[vertexOffset + 2]);
}
lineIndices.push_back(oldLineIndex - discardCount);
if (oldIndex < data.originalIndices.size())
{
originalIndices.push_back(data.originalIndices[oldIndex]);
}
if (oldIndex < data.pointInLineIndices.size())
{
pointInLineIndices.push_back(data.pointInLineIndices[oldIndex]);
}
}
data.vertices = std::move(vertices);
data.colors = std::move(colors);
data.lineIndices = std::move(lineIndices);
data.originalIndices = std::move(originalIndices);
data.pointInLineIndices = std::move(pointInLineIndices);
std::vector<PointCloudData::PointSizeGroup> sizeGroups;
for (const PointCloudData::PointSizeGroup& oldGroup :
data.customPointSizeGroups)
{
PointCloudData::PointSizeGroup newGroup;
newGroup.pointSize = oldGroup.pointSize;
for (const size_t oldIndex : oldGroup.indices)
{
if (oldIndex < remappedIndices.size() &&
remappedIndices[oldIndex] >= 0)
{
newGroup.indices.push_back(
static_cast<size_t>(remappedIndices[oldIndex]));
}
}
if (!newGroup.indices.empty())
{
sizeGroups.push_back(std::move(newGroup));
}
}
data.customPointSizeGroups = std::move(sizeGroups);
data.hasCustomPointSizes = !data.customPointSizeGroups.empty();
data.sourceLineCount -= discardCount;
data.totalLines = qMax(0, data.totalLines - discardCount);
recomputeCloudBounds(data);
rebuildPseudoColors(data, 0);
}
void PointCloudGLWidget::setPointCloudColor(PointCloudColor color)
{
m_currentColor = color;
update();
}
void PointCloudGLWidget::setPointSize(float size)
{
m_pointSize = qBound(1.0f, size, 10.0f);
update();
}
float PointCloudGLWidget::effectivePointSize() const
{
const float userSize = qMax(m_pointSize, 1.0f);
const float pixelRatio = static_cast<float>(devicePixelRatioF());
const float baseSize = userSize * qMax(pixelRatio, 1.0f);
const float fitted = fittedCameraDistance();
if (!std::isfinite(fitted) || !std::isfinite(m_distance) ||
fitted <= 1.0f || m_distance <= 1e-3f) {
return baseSize;
}
// 点精灵尺寸是屏幕像素;减半缩放系数和上限,避免拉近后点过大。
const float zoomScale = qBound(1.0f, 0.1f * fitted / m_distance, 6.0f);
return qMin(baseSize * zoomScale, 8.0f);
}
QVector3D PointCloudGLWidget::pseudoColorForZ(float z) const
{
if (!std::isfinite(z))
{
return QVector3D();
}
if (!std::isfinite(m_pseudoColorPeriod) || m_pseudoColorPeriod <= 1e-6f)
{
return m_pseudoColorDefaultPalette
? defaultHuePseudoColor(0.0f)
: m_pseudoColorStartColor;
}
const double period = static_cast<double>(m_pseudoColorPeriod);
const double cycleValue = std::floor(static_cast<double>(z) / period);
double phase = std::fmod(static_cast<double>(z), period);
if (phase < 0.0)
{
phase += period;
}
float t = static_cast<float>(phase / period);
if (m_pseudoColorDefaultPalette)
{
// 默认方案:每个周期走完整色相条,首尾同为红色,周期边界自然衔接。
return defaultHuePseudoColor(t);
}
// 自定义起止色:固定周期内连续梯度,奇数周期反向避免边界突变。
if (std::fmod(std::fabs(cycleValue), 2.0) >= 1.0)
{
t = 1.0f - t;
}
return m_pseudoColorStartColor * (1.0f - t) + m_pseudoColorEndColor * t;
}
void PointCloudGLWidget::rebuildPseudoColors(PointCloudData& data, size_t firstVertex)
{
if (data.hasColor)
{
return;
}
const size_t vertexCount = data.vertices.size() / 3;
if (!m_pseudoColorEnabled)
{
data.colors.clear();
data.pseudoColor = false;
return;
}
if (firstVertex > vertexCount)
{
firstVertex = 0;
}
data.colors.resize(vertexCount * 3);
for (size_t vertexIndex = firstVertex; vertexIndex < vertexCount; ++vertexIndex)
{
const size_t offset = vertexIndex * 3;
const QVector3D color = pseudoColorForZ(data.vertices[offset + 2]);
data.colors[offset] = color.x();
data.colors[offset + 1] = color.y();
data.colors[offset + 2] = color.z();
}
data.pseudoColor = true;
}
void PointCloudGLWidget::updatePseudoColors()
{
bool hasUncoloredCloud = false;
for (auto& data : m_pointClouds)
{
if (!data.hasColor)
{
hasUncoloredCloud = true;
rebuildPseudoColors(data, 0);
}
}
if (!hasUncoloredCloud)
{
return;
}
makeCurrent();
for (auto& data : m_pointClouds)
{
if (!data.hasColor)
{
uploadToVBO(data);
}
}
doneCurrent();
update();
}
void PointCloudGLWidget::setPseudoColorEnabled(bool enabled)
{
setPseudoColorSettings(enabled,
m_pseudoColorPeriod,
m_pseudoColorStartColor,
m_pseudoColorEndColor,
m_pseudoColorDefaultPalette);
}
void PointCloudGLWidget::setPseudoColorSettings(bool enabled, float period, const QVector3D& startColor, const QVector3D& endColor, bool defaultPalette)
{
const float safePeriod = std::isfinite(period) && period > 0.0f
? qMax(1.0f, period)
: defaultPseudoColorPeriod();
const QVector3D clampedStart = clampPseudoColor(startColor);
const QVector3D clampedEnd = clampPseudoColor(endColor);
const bool changed = m_pseudoColorEnabled != enabled ||
!qFuzzyCompare(m_pseudoColorPeriod, safePeriod) ||
m_pseudoColorStartColor != clampedStart ||
m_pseudoColorEndColor != clampedEnd ||
m_pseudoColorDefaultPalette != defaultPalette;
if (!changed)
{
return;
}
m_pseudoColorEnabled = enabled;
m_pseudoColorPeriod = safePeriod;
m_pseudoColorStartColor = clampedStart;
m_pseudoColorEndColor = clampedEnd;
m_pseudoColorDefaultPalette = defaultPalette;
updatePseudoColors();
}
void PointCloudGLWidget::setPseudoColorPeriod(float period)
{
setPseudoColorSettings(m_pseudoColorEnabled,
period,
m_pseudoColorStartColor,
m_pseudoColorEndColor,
m_pseudoColorDefaultPalette);
}
void PointCloudGLWidget::setPseudoColorColorRange(const QVector3D& startColor,
const QVector3D& endColor)
{
// 直接指定起止色表示切换到自定义梯度;默认蓝→红可通过重置恢复。
setPseudoColorSettings(m_pseudoColorEnabled,
m_pseudoColorPeriod,
startColor,
endColor,
false);
}
void PointCloudGLWidget::setPseudoColorDefaultPalette(bool enabled)
{
setPseudoColorSettings(m_pseudoColorEnabled,
m_pseudoColorPeriod,
m_pseudoColorStartColor,
m_pseudoColorEndColor,
enabled);
}
void PointCloudGLWidget::resetPseudoColorDefaults()
{
setPseudoColorSettings(true,
defaultPseudoColorPeriod(),
defaultPseudoColorStartColor(),
defaultPseudoColorEndColor(),
true);
}
float PointCloudGLWidget::defaultPseudoColorPeriod()
{
return kDefaultPseudoColorPeriod;
}
QVector3D PointCloudGLWidget::defaultPseudoColorStartColor()
{
return QVector3D(0.0f, 0.0f, 1.0f);
}
QVector3D PointCloudGLWidget::defaultPseudoColorEndColor()
{
return QVector3D(1.0f, 0.0f, 0.0f);
}
void PointCloudGLWidget::resetView()
{
QVector3D size = m_maxBound - m_minBound;
float maxSize = qMax(qMax(size.x(), size.y()), size.z());
// 默认视角:面向 XY 平面,X 向右,Y 向下
// 绕 X 轴旋转 180°:Y 轴从向上翻转为向下
m_rotationX = 180.0f;
m_rotationY = 0.0f;
m_rotationZ = 0.0f;
m_rotation = QQuaternion::fromEulerAngles(m_rotationX, m_rotationY, m_rotationZ);
m_pan = QVector3D(0, 0, 0);
m_distance = fittedCameraDistance();
CLOUD_LOG_INFO("[CloudView] resetView: size=(%.3f, %.3f, %.3f) maxSize=%.3f distance=%.3f\n",
size.x(), size.y(), size.z(), maxSize, m_distance);
emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ);
update();
}
void PointCloudGLWidget::fitToContent()
{
computeBoundingBox();
updatePseudoColors();
resetView();
}
void PointCloudGLWidget::updateProjection(const QMatrix4x4& modelView)
{
const QVector3D corners[] = {
{m_minBound.x(), m_minBound.y(), m_minBound.z()},
{m_minBound.x(), m_minBound.y(), m_maxBound.z()},
{m_minBound.x(), m_maxBound.y(), m_minBound.z()},
{m_minBound.x(), m_maxBound.y(), m_maxBound.z()},
{m_maxBound.x(), m_minBound.y(), m_minBound.z()},
{m_maxBound.x(), m_minBound.y(), m_maxBound.z()},
{m_maxBound.x(), m_maxBound.y(), m_minBound.z()},
{m_maxBound.x(), m_maxBound.y(), m_maxBound.z()}
};
float minimumDepth = FLT_MAX;
float maximumDepth = -FLT_MAX;
for (const QVector3D& corner : corners) {
const float depth = -modelView.map(corner).z();
minimumDepth = qMin(minimumDepth, depth);
maximumDepth = qMax(maximumDepth, depth);
}
constexpr float kMinimumNearPlane = 0.01f;
float nearPlane = kMinimumNearPlane;
float farPlane = 1.0f;
const float boundsDiagonal = (m_maxBound - m_minBound).length();
const float safeDiagonal = std::isfinite(boundsDiagonal)
? qMax(boundsDiagonal, 1.0f)
: 1.0f;
const float safeCameraDistance = std::isfinite(m_distance)
? qMax(m_distance, 1.0f)
: 1.0f;
if (std::isfinite(minimumDepth) && std::isfinite(maximumDepth) &&
maximumDepth > kMinimumNearPlane) {
// 点云贴近或跨过相机时保持很小的近裁剪距离,避免前侧点被裁掉。
if (minimumDepth > kMinimumNearPlane) {
nearPlane = qMax(kMinimumNearPlane, minimumDepth * 0.5f);
}
// 远裁剪面仅覆盖当前内容,并限制 near/far 比值以保证深度缓冲精度。
farPlane = qMax(nearPlane + 1.0f, maximumDepth * 1.5f);
nearPlane = qMax(nearPlane, farPlane / 100000.0f);
} else {
// The scan range can cross the current camera plane while the view
// is being updated. Keep a conservative frustum instead of letting
// all points fall outside a near/far clip interval.
nearPlane = kMinimumNearPlane;
farPlane = safeCameraDistance + safeDiagonal * 2.0f;
}
if (!std::isfinite(farPlane) || farPlane <= nearPlane) {
farPlane = nearPlane + safeCameraDistance + safeDiagonal * 2.0f;
}
const float aspect = static_cast<float>(width()) /
static_cast<float>(height() > 0 ? height() : 1);
m_projection.setToIdentity();
m_projection.perspective(45.0f, aspect, nearPlane, farPlane);
}
float PointCloudGLWidget::fittedCameraDistance(float padding) const
{
const QVector3D corners[] = {
{m_minBound.x(), m_minBound.y(), m_minBound.z()},
{m_minBound.x(), m_minBound.y(), m_maxBound.z()},
{m_minBound.x(), m_maxBound.y(), m_minBound.z()},
{m_minBound.x(), m_maxBound.y(), m_maxBound.z()},
{m_maxBound.x(), m_minBound.y(), m_minBound.z()},
{m_maxBound.x(), m_minBound.y(), m_maxBound.z()},
{m_maxBound.x(), m_maxBound.y(), m_minBound.z()},
{m_maxBound.x(), m_maxBound.y(), m_maxBound.z()}};
const float aspect = qMax(0.01f,
static_cast<float>(width()) /
static_cast<float>(height() > 0 ? height() : 1));
const float tangent = std::tan(qDegreesToRadians(45.0f * 0.5f));
const float safePadding = qMax(1.0f, padding);
float distance = minimumCameraDistance();
for (const QVector3D& corner : corners) {
const QVector3D relative =
m_rotation.rotatedVector(corner - m_center);
const float horizontalDistance =
relative.z() + std::fabs(relative.x()) * safePadding / (tangent * aspect);
const float verticalDistance =
relative.z() + std::fabs(relative.y()) * safePadding / tangent;
distance = qMax(distance, qMax(horizontalDistance, verticalDistance));
}
return qBound(10.0f, distance, 1.0e7f);
}
float PointCloudGLWidget::minimumCameraDistance() const
{
const QVector3D corners[] = {
{m_minBound.x(), m_minBound.y(), m_minBound.z()},
{m_minBound.x(), m_minBound.y(), m_maxBound.z()},
{m_minBound.x(), m_maxBound.y(), m_minBound.z()},
{m_minBound.x(), m_maxBound.y(), m_maxBound.z()},
{m_maxBound.x(), m_minBound.y(), m_minBound.z()},
{m_maxBound.x(), m_minBound.y(), m_maxBound.z()},
{m_maxBound.x(), m_maxBound.y(), m_minBound.z()},
{m_maxBound.x(), m_maxBound.y(), m_maxBound.z()}
};
float frontExtent = 0.0f;
for (const QVector3D& corner : corners) {
const QVector3D relativePosition = corner - m_center;
frontExtent = qMax(frontExtent,
m_rotation.rotatedVector(relativePosition).z());
}
const float margin = qMax(0.1f, (m_maxBound - m_minBound).length() * 0.002f);
return qBound(0.1f, frontExtent + margin, 1.0e7f);
}
void PointCloudGLWidget::zoomIn()
{
m_distance *= 0.9f;
m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f);
update();
}
void PointCloudGLWidget::zoomOut()
{
m_distance *= 1.1f;
m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f);
update();
}
void PointCloudGLWidget::setViewAngles(float rotX, float rotY, float rotZ)
{
// 归一化到 ±180°,避免往同一方向旋转时角度无限累加
m_rotationX = NormalizeAngleDegrees(rotX);
m_rotationY = NormalizeAngleDegrees(rotY);
m_rotationZ = NormalizeAngleDegrees(rotZ);
m_pan = QVector3D(0, 0, 0);
// 从欧拉角转换为四元数(ZYX顺序)
m_rotation = QQuaternion::fromEulerAngles(m_rotationX, m_rotationY, m_rotationZ);
m_distance = qMax(m_distance, minimumCameraDistance());
CLOUD_LOG_INFO("[CloudView] setViewAngles: rotX=%.1f, rotY=%.1f, rotZ=%.1f\n", rotX, rotY, rotZ);
emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ);
update();
}
void PointCloudGLWidget::clearSelectedPoints()
{
m_selectedPoints.clear();
update();
}
void PointCloudGLWidget::clearSelectedLine()
{
m_selectedLine = SelectedLineInfo();
update();
}
bool PointCloudGLWidget::selectLineByIndex(int lineIndex)
{
if (m_pointClouds.empty()) {
return false;
}
const auto& cloudData = m_pointClouds[0];
if (!cloudData.hasLineInfo) {
return false;
}
// 检查线索引是否有效
if (lineIndex < 0 || lineIndex >= cloudData.totalLines) {
return false;
}
// 计算该线上的点数
int pointCount = 0;
for (int idx : cloudData.lineIndices) {
if (idx == lineIndex) {
pointCount++;
}
}
if (pointCount == 0) {
return false;
}
// 设置选中的线
m_selectedLine.valid = true;
m_selectedLine.cloudIndex = 0;
m_selectedLine.lineIndex = lineIndex;
m_selectedLine.pointIndex = -1;
m_selectedLine.pointCount = pointCount;
m_selectedLine.mode = LineSelectMode::Vertical;
emit lineSelected(m_selectedLine);
update();
return true;
}
bool PointCloudGLWidget::selectHorizontalLineByIndex(int pointIndex)
{
if (m_pointClouds.empty()) {
return false;
}
const auto& cloudData = m_pointClouds[0];
if (!cloudData.hasLineInfo || cloudData.totalLines <= 0 || cloudData.pointsPerLine <= 0) {
return false;
}
// 检查点索引是否有效(使用原始点云的每线点数)
if (pointIndex < 0 || pointIndex >= cloudData.pointsPerLine) {
return false;
}
bool hasPointAtIndex = false;
for (const int sourceIndex : cloudData.pointInLineIndices) {
if (sourceIndex == pointIndex) {
hasPointAtIndex = true;
break;
}
}
if (!hasPointAtIndex) {
return false;
}
// 设置选中的横向线
m_selectedLine.valid = true;
m_selectedLine.cloudIndex = 0;
m_selectedLine.lineIndex = -1;
m_selectedLine.pointIndex = pointIndex;
m_selectedLine.pointCount = cloudData.totalLines; // 横向线的点数等于总线数
m_selectedLine.mode = LineSelectMode::Horizontal;
emit lineSelected(m_selectedLine);
update();
return true;
}
float PointCloudGLWidget::calculateDistance(const SelectedPointInfo& p1, const SelectedPointInfo& p2)
{
if (!p1.valid || !p2.valid) {
return 0.0f;
}
float dx = p2.x - p1.x;
float dy = p2.y - p1.y;
float dz = p2.z - p1.z;
return std::sqrt(dx * dx + dy * dy + dz * dz);
}
void PointCloudGLWidget::updateSelectedPointCoord(int index, float x, float y, float z)
{
if (index < 0 || index >= m_selectedPoints.size()) {
return;
}
if (!m_selectedPoints[index].valid) {
return;
}
// 更新选中点的坐标
m_selectedPoints[index].x = x;
m_selectedPoints[index].y = y;
m_selectedPoints[index].z = z;
CLOUD_LOG_INFO("[CloudView] Updated selected point %d to (%.3f, %.3f, %.3f)\n", index, x, y, z);
// 刷新显示
update();
}
void PointCloudGLWidget::setSelectedPointCoord(int index, float x, float y, float z)
{
if (index < 0 || index >= MAX_SELECTED_POINTS) {
return;
}
// 如果索引位置不存在,扩展列表
while (m_selectedPoints.size() <= index) {
m_selectedPoints.append(SelectedPointInfo());
}
m_selectedPoints[index].valid = true;
m_selectedPoints[index].x = x;
m_selectedPoints[index].y = y;
m_selectedPoints[index].z = z;
m_selectedPoints[index].cloudIndex = -1;
m_selectedPoints[index].lineIndex = -1;
m_selectedPoints[index].pointIndexInLine = -1;
CLOUD_LOG_INFO("[CloudView] Set selected point %d to (%.3f, %.3f, %.3f)\n", index, x, y, z);
// 刷新显示
update();
}
QVector<QVector3D> PointCloudGLWidget::getSelectedLinePoints() const
{
QVector<QVector3D> points;
if (!m_selectedLine.valid || m_selectedLine.cloudIndex < 0) {
return points;
}
if (m_selectedLine.cloudIndex >= static_cast<int>(m_pointClouds.size())) {
return points;
}
const auto& cloudData = m_pointClouds[m_selectedLine.cloudIndex];
if (!cloudData.hasLineInfo) {
return points;
}
if (m_selectedLine.mode == LineSelectMode::Vertical) {
// 纵向选线:获取同一条扫描线上的所有点
for (size_t i = 0; i < cloudData.lineIndices.size(); ++i) {
if (cloudData.lineIndices[i] == m_selectedLine.lineIndex) {
size_t vertIdx = i * 3;
if (vertIdx + 2 < cloudData.vertices.size()) {
points.append(QVector3D(
cloudData.vertices[vertIdx],
cloudData.vertices[vertIdx + 1],
cloudData.vertices[vertIdx + 2]));
}
}
}
} else {
// 横向选线:获取所有线的相同索引点
if (cloudData.pointsPerLine > 0 && m_selectedLine.pointIndex >= 0) {
for (size_t i = 0; i < cloudData.pointInLineIndices.size(); ++i) {
if (cloudData.pointInLineIndices[i] == m_selectedLine.pointIndex) {
size_t vertIdx = i * 3;
if (vertIdx + 2 < cloudData.vertices.size()) {
points.append(QVector3D(
cloudData.vertices[vertIdx],
cloudData.vertices[vertIdx + 1],
cloudData.vertices[vertIdx + 2]));
}
}
}
}
}
return points;
}
void PointCloudGLWidget::setListHighlightPoint(const QVector3D& point)
{
m_hasListHighlightPoint = true;
m_listHighlightPoint = point;
update();
}
void PointCloudGLWidget::clearListHighlightPoint()
{
m_hasListHighlightPoint = false;
update();
}
void PointCloudGLWidget::transformAllClouds(const QMatrix4x4& matrix)
{
for (auto& cloudData : m_pointClouds) {
for (size_t i = 0; i + 2 < cloudData.vertices.size(); i += 3) {
QVector3D pt(cloudData.vertices[i], cloudData.vertices[i + 1], cloudData.vertices[i + 2]);
QVector3D transformed = matrix.map(pt);
cloudData.vertices[i] = transformed.x();
cloudData.vertices[i + 1] = transformed.y();
cloudData.vertices[i + 2] = transformed.z();
}
recomputeCloudBounds(cloudData);
}
// 重新上传 VBO(顶点数据已变更)
makeCurrent();
for (auto& cloudData : m_pointClouds) {
uploadToVBO(cloudData);
}
doneCurrent();
computeBoundingBox();
updatePseudoColors();
resetView();
update();
}
size_t PointCloudGLWidget::rotateCloudsByZThreshold(const QMatrix4x4& rotMatrix, float zThreshold)
{
size_t rotatedCount = 0;
for (auto& cloudData : m_pointClouds) {
for (size_t i = 0; i + 2 < cloudData.vertices.size(); i += 3) {
if (cloudData.vertices[i + 2] < zThreshold) {
QVector3D pt(cloudData.vertices[i], cloudData.vertices[i + 1], cloudData.vertices[i + 2]);
QVector3D transformed = rotMatrix.map(pt);
cloudData.vertices[i] = transformed.x();
cloudData.vertices[i + 1] = transformed.y();
cloudData.vertices[i + 2] = transformed.z();
++rotatedCount;
}
}
recomputeCloudBounds(cloudData);
}
if (rotatedCount > 0) {
makeCurrent();
for (auto& cloudData : m_pointClouds) {
uploadToVBO(cloudData);
}
doneCurrent();
computeBoundingBox();
updatePseudoColors();
resetView();
update();
}
return rotatedCount;
}
size_t PointCloudGLWidget::getAllCloudsByLines(std::vector<std::vector<SVzNL3DPosition>>& scanLines) const
{
scanLines.clear();
size_t totalPoints = 0;
for (const auto& cloudData : m_pointClouds) {
const size_t displayCount = cloudData.vertices.size() / 3;
if (displayCount == 0) continue;
if (cloudData.hasLineInfo && cloudData.totalLines > 0 && cloudData.pointsPerLine > 0) {
// 有线信息:用 pointsPerLine 还原每条线的完整点数,过滤掉的 (0,0,0) 点补零
const int lines = cloudData.totalLines;
const int ptsPerLine = cloudData.pointsPerLine;
// 初始化所有线,每条线 pointsPerLine 个零点
const size_t baseIdx = scanLines.size();
for (int i = 0; i < lines; ++i) {
std::vector<SVzNL3DPosition> line(ptsPerLine);
memset(line.data(), 0, sizeof(SVzNL3DPosition) * ptsPerLine);
for (int j = 0; j < ptsPerLine; ++j) {
line[j].nPointIdx = j;
}
scanLines.push_back(std::move(line));
}
// 用实际显示的点填充对应位置
for (size_t i = 0; i < displayCount; ++i) {
const int lineIdx = (i < cloudData.lineIndices.size()) ? cloudData.lineIndices[i] : 0;
const int ptInLine = (i < cloudData.pointInLineIndices.size()) ? cloudData.pointInLineIndices[i] : static_cast<int>(i);
if (lineIdx >= 0 && lineIdx < lines && ptInLine >= 0 && ptInLine < ptsPerLine) {
auto& pos = scanLines[baseIdx + lineIdx][ptInLine];
pos.pt3D.x = cloudData.vertices[i * 3];
pos.pt3D.y = cloudData.vertices[i * 3 + 1];
pos.pt3D.z = cloudData.vertices[i * 3 + 2];
pos.nPointIdx = ptInLine;
}
}
totalPoints += static_cast<size_t>(lines) * ptsPerLine;
} else {
// 无线信息,每个点云作为一条线
std::vector<SVzNL3DPosition> line;
line.reserve(displayCount);
for (size_t i = 0; i < displayCount; ++i) {
SVzNL3DPosition pos;
memset(&pos, 0, sizeof(pos));
pos.nPointIdx = static_cast<int>(i);
pos.pt3D.x = cloudData.vertices[i * 3];
pos.pt3D.y = cloudData.vertices[i * 3 + 1];
pos.pt3D.z = cloudData.vertices[i * 3 + 2];
line.push_back(pos);
}
totalPoints += displayCount;
scanLines.push_back(std::move(line));
}
}
return totalPoints;
}
void PointCloudGLWidget::computeBoundingBox()
{
float minX = FLT_MAX, minY = FLT_MAX, minZ = FLT_MAX;
float maxX = -FLT_MAX, maxY = -FLT_MAX, maxZ = -FLT_MAX;
bool hasContent = false;
auto includePoint = [&](float x, float y, float z) {
minX = qMin(minX, x);
minY = qMin(minY, y);
minZ = qMin(minZ, z);
maxX = qMax(maxX, x);
maxY = qMax(maxY, y);
maxZ = qMax(maxZ, z);
hasContent = true;
};
auto includeSegment = [&](const LineSegment& segment) {
includePoint(segment.x1, segment.y1, segment.z1);
includePoint(segment.x2, segment.y2, segment.z2);
};
for (const auto& cloudData : m_pointClouds) {
if (!cloudData.hasBounds) {
continue;
}
includePoint(cloudData.minBound.x(), cloudData.minBound.y(), cloudData.minBound.z());
includePoint(cloudData.maxBound.x(), cloudData.maxBound.y(), cloudData.maxBound.z());
}
for (const LineSegment& segment : m_lineSegments) {
includeSegment(segment);
}
for (const LineSegment& segment : m_basicShapeSegments) {
includeSegment(segment);
}
for (const SurfaceQuad& surface : m_basicShapeSurfaces) {
includePoint(surface.p1.x(), surface.p1.y(), surface.p1.z());
includePoint(surface.p2.x(), surface.p2.y(), surface.p2.z());
includePoint(surface.p3.x(), surface.p3.y(), surface.p3.z());
includePoint(surface.p4.x(), surface.p4.y(), surface.p4.z());
}
for (const BasicShapePoint& point : m_basicShapePoints) {
includePoint(point.x, point.y, point.z);
}
for (const PosePoint& pose : m_posePoints) {
const float scale = qMax(0.0f, pose.scale);
includePoint(pose.x - scale, pose.y - scale, pose.z - scale);
includePoint(pose.x + scale, pose.y + scale, pose.z + scale);
}
if (!hasContent) {
m_minBound = QVector3D(-50, -50, -50);
m_maxBound = QVector3D(50, 50, 50);
m_center = QVector3D(0, 0, 0);
return;
}
m_minBound = QVector3D(minX, minY, minZ);
m_maxBound = QVector3D(maxX, maxY, maxZ);
m_center = (m_minBound + m_maxBound) / 2.0f;
}
SelectedPointInfo PointCloudGLWidget::pickPoint(int screenX, int screenY)
{
SelectedPointInfo result;
if (m_pointClouds.empty()) {
return result;
}
// 视口(Qt 窗口即视口);GLES2 无 GL_MODELVIEW_MATRIX / gluProject,用与渲染一致的矩阵手动投影
const GLint viewport[4] = {0, 0, static_cast<GLint>(width()), static_cast<GLint>(height())};
const QMatrix4x4 modelView = m_view * m_model;
const QMatrix4x4 mvp = m_projection * modelView;
// 转换屏幕 Y 坐标(OpenGL Y 轴向上)
const int glScreenY = viewport[3] - screenY;
float minScreenDist = FLT_MAX;
size_t bestIndex = 0;
int bestCloudIndex = -1;
int bestLineIndex = -1;
float bestX = 0, bestY = 0, bestZ = 0;
// 遍历所有点,计算屏幕投影距离
for (size_t cloudIdx = 0; cloudIdx < m_pointClouds.size(); ++cloudIdx) {
const auto& cloudData = m_pointClouds[cloudIdx];
for (size_t i = 0; i < cloudData.vertices.size(); i += 3) {
const float wx = cloudData.vertices[i];
const float wy = cloudData.vertices[i + 1];
const float wz = cloudData.vertices[i + 2];
// 将世界坐标投影到屏幕(等价于 gluProject)
const QVector4D clip = mvp * QVector4D(wx, wy, wz, 1.0f);
if (std::fabs(clip.w()) < 1e-9f) {
continue;
}
const float ndcX = clip.x() / clip.w();
const float ndcY = clip.y() / clip.w();
const float sx = viewport[0] + (ndcX + 1.0f) * 0.5f * viewport[2];
const float sy = viewport[1] + (ndcY + 1.0f) * 0.5f * viewport[3];
// 计算屏幕距离
const float dx = sx - screenX;
const float dy = sy - glScreenY;
const float screenDist = dx * dx + dy * dy;
if (screenDist < minScreenDist) {
minScreenDist = screenDist;
bestIndex = i / 3;
bestCloudIndex = static_cast<int>(cloudIdx);
bestX = wx;
bestY = wy;
bestZ = wz;
// 获取线索引
if (cloudData.hasLineInfo && bestIndex < cloudData.lineIndices.size()) {
bestLineIndex = cloudData.lineIndices[bestIndex];
} else {
bestLineIndex = -1;
}
}
}
}
// 屏幕距离阈值(像素),20像素内认为选中
float threshold = 20.0f * 20.0f;
if (minScreenDist < threshold) {
result.valid = true;
result.index = bestIndex;
result.cloudIndex = bestCloudIndex;
result.lineIndex = bestLineIndex;
result.x = bestX;
result.y = bestY;
result.z = bestZ;
// 计算点在线中的原始索引
if (bestLineIndex >= 0 && bestCloudIndex >= 0) {
const auto& cloudData = m_pointClouds[bestCloudIndex];
// 使用预计算的线内索引(支持不等长线)
if (bestIndex < cloudData.pointInLineIndices.size()) {
result.pointIndexInLine = cloudData.pointInLineIndices[bestIndex];
}
}
CLOUD_LOG_INFO("[CloudView] Point selected: (%.3f, %.3f, %.3f) lineIndex=%d pointIndexInLine=%d screenDist=%.1f\n",
bestX, bestY, bestZ, bestLineIndex, result.pointIndexInLine, std::sqrt(minScreenDist));
} else {
CLOUD_LOG_INFO("[CloudView] No point selected, minScreenDist=%.1f\n", std::sqrt(minScreenDist));
}
return result;
}
void PointCloudGLWidget::drawSelectedPoints()
{
QVector<float> positions;
QVector<float> colors;
// 绘制选中的点(橙色)
for (const auto& pt : m_selectedPoints) {
if (pt.valid) {
positions.append(pt.x);
positions.append(pt.y);
positions.append(pt.z);
colors.append(1.0f);
colors.append(0.5f);
colors.append(0.0f);
}
}
// 绘制列表高亮点(蓝色,与选点区分)
if (m_hasListHighlightPoint) {
positions.append(m_listHighlightPoint.x());
positions.append(m_listHighlightPoint.y());
positions.append(m_listHighlightPoint.z());
colors.append(0.0f);
colors.append(0.5f);
colors.append(1.0f);
}
if (positions.isEmpty()) {
return;
}
drawPrimitives(m_mvp,
positions.constData(),
positions.size() / 3,
GL_POINTS,
colors.constData(),
QVector3D(1.0f, 1.0f, 1.0f),
10.0f,
1.0f,
false);
}
void PointCloudGLWidget::drawMeasurementLine()
{
if (m_selectedPoints.size() < 2) {
return;
}
const auto& p1 = m_selectedPoints[0];
const auto& p2 = m_selectedPoints[1];
if (!p1.valid || !p2.valid) {
return;
}
const float positions[] = {
p1.x, p1.y, p1.z,
p2.x, p2.y, p2.z};
drawPrimitives(m_mvp,
positions,
2,
GL_LINES,
nullptr,
QVector3D(0.0f, 1.0f, 0.0f),
1.0f,
1.0f,
false);
}
void PointCloudGLWidget::drawSelectedLine()
{
if (!m_selectedLine.valid || m_selectedLine.cloudIndex < 0) {
return;
}
if (m_selectedLine.cloudIndex >= static_cast<int>(m_pointClouds.size())) {
return;
}
const auto& cloudData = m_pointClouds[m_selectedLine.cloudIndex];
if (!cloudData.hasLineInfo) {
return;
}
// 高亮显示选中线上的所有点(黄色)
QVector<float> positions;
if (m_selectedLine.mode == LineSelectMode::Vertical) {
// 纵向选线:显示同一条扫描线上的所有点
for (size_t i = 0; i < cloudData.lineIndices.size(); ++i) {
if (cloudData.lineIndices[i] == m_selectedLine.lineIndex) {
const size_t vertIdx = i * 3;
if (vertIdx + 2 < cloudData.vertices.size()) {
positions.append(cloudData.vertices[vertIdx]);
positions.append(cloudData.vertices[vertIdx + 1]);
positions.append(cloudData.vertices[vertIdx + 2]);
}
}
}
} else {
// 横向选线:显示所有线的相同原始索引点
if (cloudData.pointsPerLine > 0 && m_selectedLine.pointIndex >= 0) {
for (size_t i = 0; i < cloudData.pointInLineIndices.size(); ++i) {
if (cloudData.pointInLineIndices[i] == m_selectedLine.pointIndex) {
const size_t vertIdx = i * 3;
if (vertIdx + 2 < cloudData.vertices.size()) {
positions.append(cloudData.vertices[vertIdx]);
positions.append(cloudData.vertices[vertIdx + 1]);
positions.append(cloudData.vertices[vertIdx + 2]);
}
}
}
}
}
if (positions.isEmpty()) {
return;
}
drawPrimitives(m_mvp,
positions.constData(),
positions.size() / 3,
GL_POINTS,
nullptr,
QVector3D(1.0f, 1.0f, 0.0f),
3.0f,
1.0f,
false);
}
void PointCloudGLWidget::drawAxis()
{
// 在右下角绘制坐标系指示器
// 坐标系定义:X向右,Y向下,Z朝后
// X轴:红色,指向右
// Y轴:绿色,指向下
// Z轴:蓝色,指向后(远离观察者)
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
int axisSize = 60; // 坐标系区域大小(像素)
int margin = 10; // 距离边缘的边距
int axisX = viewport[2] - axisSize - margin; // 右下角 X
int axisY = margin; // 右下角 Y(OpenGL Y 轴向上)
// 正交投影 + 视图变换(等价于原 glOrtho + glTranslatef + glMultMatrixf)
QMatrix4x4 projection;
projection.setToIdentity();
projection.ortho(0, viewport[2], 0, viewport[3], -100, 100);
QMatrix4x4 modelView;
modelView.setToIdentity();
modelView.translate(axisX + axisSize / 2.0f, axisY + axisSize / 2.0f, 0.0f);
QMatrix4x4 rotation;
rotation.rotate(m_rotation);
modelView *= rotation;
const QMatrix4x4 mvp = projection * modelView;
const float axisLength = axisSize * 0.4f; // 坐标轴长度
const float positions[] = {
0.0f, 0.0f, 0.0f, axisLength, 0.0f, 0.0f, // X 轴 - 红色(向右)
0.0f, 0.0f, 0.0f, 0.0f, axisLength, 0.0f, // Y 轴 - 绿色(向下)
0.0f, 0.0f, 0.0f, 0.0f, 0.0f, axisLength // Z 轴 - 蓝色(向后)
};
const float colors[] = {
1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f
};
// 关闭深度测试,确保坐标系始终可见
drawPrimitives(mvp, positions, 6, GL_LINES, colors,
QVector3D(1.0f, 1.0f, 1.0f), 1.0f, 1.0f, false);
}
void PointCloudGLWidget::drawAxisLabels()
{
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
int axisSize = 60;
int margin = 10;
float axisLength = axisSize * 0.4f;
// 坐标轴中心在 OpenGL 坐标系中的位置
float centerGlX = viewport[2] - axisSize / 2.0f - margin;
float centerGlY = axisSize / 2.0f + margin;
// 对各轴端点应用当前视图旋转
QMatrix4x4 rotMatrix;
rotMatrix.rotate(m_rotation);
QVector3D xEnd = rotMatrix.map(QVector3D(axisLength, 0, 0));
QVector3D yEnd = rotMatrix.map(QVector3D(0, axisLength, 0));
QVector3D zEnd = rotMatrix.map(QVector3D(0, 0, axisLength));
// 转换为 Qt widget 坐标(Y 轴翻转:OpenGL Y 向上 → Qt Y 向下)
auto toWidgetPos = [&](const QVector3D& v) -> QPointF {
float glX = centerGlX + v.x();
float glY = centerGlY + v.y();
return QPointF(glX, viewport[3] - glY);
};
QPointF xPos = toWidgetPos(xEnd);
QPointF yPos = toWidgetPos(yEnd);
QPointF zPos = toWidgetPos(zEnd);
// 使用 QPainter 绘制文字标注
QPainter painter(this);
painter.setRenderHint(QPainter::TextAntialiasing);
QFont font = painter.font();
font.setBold(true);
font.setPointSize(14);
painter.setFont(font);
// 文字偏移:向轴方向外侧偏移,避免与轴线重叠
int offset = 4;
painter.setPen(QColor(255, 80, 80)); // 红色
painter.drawText(xPos.x() + offset, xPos.y() + offset, "X");
painter.setPen(QColor(80, 255, 80)); // 绿色
painter.drawText(yPos.x() + offset, yPos.y() + offset, "Y");
painter.setPen(QColor(80, 128, 255)); // 蓝色
painter.drawText(zPos.x() + offset, zPos.y() + offset, "Z");
painter.end();
}
void PointCloudGLWidget::drawSelectionInfoOverlay()
{
QVector<SelectedPointInfo> validPoints;
for (const SelectedPointInfo& point : m_selectedPoints) {
if (point.valid) {
validPoints.append(point);
}
}
if (validPoints.isEmpty()) {
return;
}
QStringList lines;
for (int i = 0; i < validPoints.size() && i < MAX_SELECTED_POINTS; ++i) {
const SelectedPointInfo& point = validPoints[i];
lines.append(QString("P%1 X: %2 mm Y: %3 mm Z: %4 mm")
.arg(i + 1)
.arg(point.x, 0, 'f', 3)
.arg(point.y, 0, 'f', 3)
.arg(point.z, 0, 'f', 3));
}
if (validPoints.size() >= 2) {
const SelectedPointInfo& p1 = validPoints[0];
const SelectedPointInfo& p2 = validPoints[1];
const float dx = p2.x - p1.x;
const float dy = p2.y - p1.y;
const float dz = p2.z - p1.z;
const float distance = calculateDistance(p1, p2);
const int precision = distance < 0.001f ? 6 : (distance < 1.0f ? 4 : 3);
lines.append(QString("Distance: %1 mm").arg(distance, 0, 'f', precision));
lines.append(QString("dX: %1 mm dY: %2 mm dZ: %3 mm")
.arg(dx, 0, 'f', precision)
.arg(dy, 0, 'f', precision)
.arg(dz, 0, 'f', precision));
}
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::TextAntialiasing);
QFont font = painter.font();
font.setPointSize(10);
painter.setFont(font);
const QFontMetrics metrics(font);
int textWidth = 0;
for (const QString& line : lines) {
textWidth = qMax(textWidth, metrics.horizontalAdvance(line));
}
const int margin = 10;
const int lineHeight = metrics.lineSpacing();
const int textX = qMax(margin, width() - textWidth - margin);
int textY = margin + metrics.ascent();
for (int i = 0; i < lines.size(); ++i) {
painter.setPen(QColor(240, 244, 248));
painter.drawText(textX, textY, lines[i]);
textY += lineHeight;
}
}
void PointCloudGLWidget::mousePressEvent(QMouseEvent* event)
{
m_lastMousePos = event->pos();
if (event->button() == Qt::LeftButton) {
// Alt+左键:依次选择两个点并测距
if (event->modifiers() & Qt::AltModifier) {
makeCurrent();
SelectedPointInfo point = pickPoint(event->pos().x(), event->pos().y());
doneCurrent();
if (point.valid) {
if (m_selectedPoints.size() >= MAX_SELECTED_POINTS) {
m_selectedPoints.clear();
}
m_selectedPoints.append(point);
emit pointSelected(point);
if (m_selectedPoints.size() == 2) {
const float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]);
const float dx = m_selectedPoints[1].x - m_selectedPoints[0].x;
const float dy = m_selectedPoints[1].y - m_selectedPoints[0].y;
const float dz = m_selectedPoints[1].z - m_selectedPoints[0].z;
emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance);
emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz);
}
update();
}
} else if (event->modifiers() & Qt::ControlModifier) {
// Ctrl+左键:选择一个点
makeCurrent();
SelectedPointInfo point = pickPoint(event->pos().x(), event->pos().y());
doneCurrent();
if (point.valid) {
m_selectedPoints.clear();
m_selectedPoints.append(point);
emit pointSelected(point);
update();
}
} else if (event->modifiers() & Qt::ShiftModifier) {
// Shift+左键:选线
makeCurrent();
SelectedPointInfo point = pickPoint(event->pos().x(), event->pos().y());
doneCurrent();
if (point.valid && point.lineIndex >= 0) {
if (m_lineSelectMode == LineSelectMode::Vertical) {
// 纵向选线:选中该点所在的线
m_selectedLine.valid = true;
m_selectedLine.cloudIndex = point.cloudIndex;
m_selectedLine.lineIndex = point.lineIndex;
m_selectedLine.pointIndex = -1;
m_selectedLine.mode = LineSelectMode::Vertical;
// 计算该线上的点数
int pointCount = 0;
if (point.cloudIndex >= 0 && point.cloudIndex < static_cast<int>(m_pointClouds.size())) {
const auto& cloudData = m_pointClouds[point.cloudIndex];
for (int idx : cloudData.lineIndices) {
if (idx == point.lineIndex) {
pointCount++;
}
}
}
m_selectedLine.pointCount = pointCount;
} else {
// 横向选线:选中所有线的相同索引点
m_selectedLine.valid = true;
m_selectedLine.cloudIndex = point.cloudIndex;
m_selectedLine.lineIndex = -1;
m_selectedLine.pointIndex = point.pointIndexInLine;
m_selectedLine.mode = LineSelectMode::Horizontal;
// 横向线的点数等于总线数
if (point.cloudIndex >= 0 && point.cloudIndex < static_cast<int>(m_pointClouds.size())) {
m_selectedLine.pointCount = m_pointClouds[point.cloudIndex].totalLines;
}
}
emit lineSelected(m_selectedLine);
update();
}
} else {
m_leftButtonPressed = true;
}
} else if (event->button() == Qt::RightButton) {
m_rightButtonPressed = true;
} else if (event->button() == Qt::MiddleButton) {
m_middleButtonPressed = true;
}
}
void PointCloudGLWidget::mouseDoubleClickEvent(QMouseEvent* event)
{
if (event->button() == Qt::LeftButton) {
// 双击左键:选点(与 Ctrl+左键 相同逻辑)
makeCurrent();
SelectedPointInfo point = pickPoint(event->pos().x(), event->pos().y());
doneCurrent();
if (point.valid) {
if (m_measureDistanceEnabled) {
// 启用测距:最多保留两个点
if (m_selectedPoints.size() >= MAX_SELECTED_POINTS) {
m_selectedPoints.clear();
}
m_selectedPoints.append(point);
emit pointSelected(point);
if (m_selectedPoints.size() == 2) {
const float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]);
const float dx = m_selectedPoints[1].x - m_selectedPoints[0].x;
const float dy = m_selectedPoints[1].y - m_selectedPoints[0].y;
const float dz = m_selectedPoints[1].z - m_selectedPoints[0].z;
emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance);
emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz);
}
} else {
// 未启用测距:只保留一个点
m_selectedPoints.clear();
m_selectedPoints.append(point);
emit pointSelected(point);
}
update();
}
}
}
void PointCloudGLWidget::mouseMoveEvent(QMouseEvent* event)
{
QPoint delta = event->pos() - m_lastMousePos;
m_lastMousePos = event->pos();
if (m_leftButtonPressed) {
// 左键拖动:基于当前物体坐标系的旋转
// 鼠标水平移动 -> 绕当前Y轴旋转
// 鼠标垂直移动 -> 绕当前X轴旋转
// 计算旋转角度
float angleX = delta.y() * 0.5f; // 垂直移动
float angleY = delta.x() * 0.5f; // 水平移动
// 创建增量旋转四元数(在相机空间中)
// 先绕X轴旋转(俯仰),再绕Y轴旋转(偏航)
QQuaternion deltaRotationX = QQuaternion::fromAxisAndAngle(QVector3D(1, 0, 0), angleX);
QQuaternion deltaRotationY = QQuaternion::fromAxisAndAngle(QVector3D(0, 1, 0), angleY);
QQuaternion deltaRotation = deltaRotationY * deltaRotationX;
// 应用增量旋转(左乘,相机空间旋转)
m_rotation = deltaRotation * m_rotation;
// 更新欧拉角(用于显示),归一化到 ±180°
m_rotationX = NormalizeAngleDegrees(m_rotationX + angleX);
m_rotationY = NormalizeAngleDegrees(m_rotationY + angleY);
// 发送角度变化信号
m_distance = qMax(m_distance, minimumCameraDistance());
emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ);
update();
} else if (m_middleButtonPressed) {
float factor = m_distance * 0.002f;
m_pan.setX(m_pan.x() - delta.x() * factor);
m_pan.setY(m_pan.y() + delta.y() * factor);
update();
} else if (m_rightButtonPressed) {
// 右键拖动:绕视线方向旋转(滚转)(修正方向)
float angle = -delta.x() * 0.5f; // 取反修正方向
QQuaternion deltaRotation = QQuaternion::fromAxisAndAngle(QVector3D(0, 0, 1), angle);
m_rotation = deltaRotation * m_rotation;
m_rotationZ = NormalizeAngleDegrees(m_rotationZ + angle);
m_distance = qMax(m_distance, minimumCameraDistance());
emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ);
update();
}
}
void PointCloudGLWidget::mouseReleaseEvent(QMouseEvent* event)
{
if (event->button() == Qt::LeftButton) {
m_leftButtonPressed = false;
} else if (event->button() == Qt::RightButton) {
m_rightButtonPressed = false;
} else if (event->button() == Qt::MiddleButton) {
m_middleButtonPressed = false;
}
}
void PointCloudGLWidget::wheelEvent(QWheelEvent* event)
{
float delta = event->angleDelta().y() / 120.0f;
m_distance *= (1.0f - delta * 0.1f);
m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f);
update();
}
void PointCloudGLWidget::keyPressEvent(QKeyEvent* event)
{
if (event->key() == Qt::Key_Escape) {
m_selectedPoints.clear();
update();
event->accept();
} else if (event->key() == Qt::Key_Space) {
resetView();
} else {
QOpenGLWidget::keyPressEvent(event);
}
}
bool PointCloudGLWidget::getFirstCloudData(PointCloudXYZ& cloud) const
{
if (m_pointClouds.empty()) {
return false;
}
const auto& cloudData = m_pointClouds[0];
cloud.clear();
cloud.reserve(cloudData.vertices.size() / 3);
for (size_t i = 0; i < cloudData.vertices.size(); i += 3) {
Point3D pt;
pt.x = cloudData.vertices[i];
pt.y = cloudData.vertices[i + 1];
pt.z = cloudData.vertices[i + 2];
int lineIdx = 0;
if (cloudData.hasLineInfo && (i / 3) < cloudData.lineIndices.size()) {
lineIdx = cloudData.lineIndices[i / 3];
}
const int pointIdx = ((i / 3) < cloudData.pointInLineIndices.size())
? cloudData.pointInLineIndices[i / 3] : static_cast<int>(i / 3);
cloud.push_back(pt, lineIdx, pointIdx);
}
return true;
}
void PointCloudGLWidget::replaceFirstCloud(const PointCloudXYZ& cloud, const QString& name)
{
if (m_pointClouds.empty()) {
addPointCloud(cloud, name);
return;
}
// 保留第一个点云的颜色索引
int colorIndex = m_pointClouds[0].colorIndex;
// 重新构建数据
PointCloudData data;
data.name = name;
data.hasColor = false;
data.hasLineInfo = !cloud.lineIndices.empty();
data.colorIndex = colorIndex;
data.vertices.reserve(cloud.size() * 3);
data.totalLines = 0;
data.pointsPerLine = 0;
const float EPSILON = 1e-6f;
int previousLineIndex = -1;
int fallbackPointIndex = 0;
int maxPointCount = 0;
for (size_t i = 0; i < cloud.points.size(); ++i) {
const auto& pt = cloud.points[i];
const int lineIdx = (i < cloud.lineIndices.size()) ? cloud.lineIndices[i] : -1;
if (lineIdx != previousLineIndex) {
fallbackPointIndex = 0;
previousLineIndex = lineIdx;
}
int pointIdx = (i < cloud.pointInLineIndices.size())
? cloud.pointInLineIndices[i] : -1;
if (pointIdx < 0) {
pointIdx = fallbackPointIndex;
}
maxPointCount = qMax(maxPointCount, pointIdx + 1);
if (!std::isfinite(pt.x) || !std::isfinite(pt.y) || !std::isfinite(pt.z)) {
++fallbackPointIndex;
continue;
}
// 显示时过滤 (0,0,0) 点
if (std::fabs(pt.x) < EPSILON && std::fabs(pt.y) < EPSILON && std::fabs(pt.z) < EPSILON) {
++fallbackPointIndex;
continue;
}
data.vertices.push_back(pt.x);
data.vertices.push_back(pt.y);
data.vertices.push_back(pt.z);
expandCloudBounds(data, pt.x, pt.y, pt.z);
// 保存原始索引
data.originalIndices.push_back(static_cast<int>(i));
data.pointInLineIndices.push_back(pointIdx);
if (data.hasLineInfo) {
// 与顶点保持一一对应;缺失的来源线号以 -1 表示。
data.lineIndices.push_back(lineIdx);
if (lineIdx >= 0 && lineIdx + 1 > data.totalLines) {
data.totalLines = lineIdx + 1;
}
}
++fallbackPointIndex;
}
// 计算每线点数
if (data.totalLines > 0) {
data.pointsPerLine = maxPointCount;
}
// 释放旧的 VBO
makeCurrent();
releaseVBO(m_pointClouds[0]);
m_pointClouds[0] = std::move(data);
// 上传新的 VBO
uploadToVBO(m_pointClouds[0]);
doneCurrent();
computeBoundingBox();
updatePseudoColors();
resetView();
update();
}
void PointCloudGLWidget::addLineSegments(const QVector<LineSegment>& segments)
{
m_lineSegments.append(segments);
update();
}
void PointCloudGLWidget::clearLineSegments()
{
m_lineSegments.clear();
update();
}
void PointCloudGLWidget::setBasicShapeSegments(const QVector<LineSegment>& segments)
{
m_basicShapeSegments = segments;
update();
}
void PointCloudGLWidget::appendBasicShapeSegments(const QVector<LineSegment>& segments)
{
if (segments.isEmpty()) {
return;
}
for (const LineSegment& segment : segments) {
m_basicShapeSegments.append(segment);
}
update();
}
void PointCloudGLWidget::appendBasicShapeSurfaces(const QVector<SurfaceQuad>& surfaces)
{
if (surfaces.isEmpty()) {
return;
}
for (const SurfaceQuad& surface : surfaces) {
m_basicShapeSurfaces.append(surface);
}
update();
}
void PointCloudGLWidget::appendBasicShapePoints(const QVector<BasicShapePoint>& points)
{
if (points.isEmpty()) {
return;
}
for (const BasicShapePoint& point : points) {
m_basicShapePoints.append(point);
}
update();
}
void PointCloudGLWidget::clearBasicShapeSegments()
{
m_basicShapeSegments.clear();
m_basicShapeSurfaces.clear();
m_basicShapePoints.clear();
update();
}
void PointCloudGLWidget::addPosePoints(const QVector<PosePoint>& poses)
{
m_posePoints.append(poses);
computeBoundingBox();
update();
}
void PointCloudGLWidget::clearPosePoints()
{
m_posePoints.clear();
update();
}
void PointCloudGLWidget::drawLineSegments()
{
drawLineSegmentList(m_lineSegments);
}
void PointCloudGLWidget::drawBasicShapeSurfaces()
{
if (m_basicShapeSurfaces.isEmpty()) {
return;
}
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDepthMask(GL_FALSE);
for (const SurfaceQuad& surface : m_basicShapeSurfaces) {
// GLES2 无 GL_QUADS,四边形拆成两个三角形 (p1,p2,p3) 和 (p1,p3,p4)
const float positions[] = {
surface.p1.x(), surface.p1.y(), surface.p1.z(),
surface.p2.x(), surface.p2.y(), surface.p2.z(),
surface.p3.x(), surface.p3.y(), surface.p3.z(),
surface.p1.x(), surface.p1.y(), surface.p1.z(),
surface.p3.x(), surface.p3.y(), surface.p3.z(),
surface.p4.x(), surface.p4.y(), surface.p4.z(),
};
drawPrimitives(m_mvp, positions, 6, GL_TRIANGLES, nullptr,
QVector3D(surface.r, surface.g, surface.b),
1.0f, surface.a, true);
}
glDepthMask(GL_TRUE);
glDisable(GL_BLEND);
}
void PointCloudGLWidget::drawBasicShapeSegments()
{
drawLineSegmentList(m_basicShapeSegments);
}
void PointCloudGLWidget::drawBasicShapePoints()
{
if (m_basicShapePoints.isEmpty()) {
return;
}
for (const BasicShapePoint& point : m_basicShapePoints) {
const float positions[] = {point.x, point.y, point.z};
drawPrimitives(m_mvp, positions, 1, GL_POINTS, nullptr,
QVector3D(point.r, point.g, point.b),
point.pointSize, 1.0f, false);
}
}
void PointCloudGLWidget::drawLineSegmentList(const QVector<LineSegment>& segments)
{
if (segments.isEmpty()) {
return;
}
// 按线宽分组绘制
// 收集所有不同的线宽值(0 表示默认 2.0f)
std::map<float, QVector<int>> widthGroups;
for (int i = 0; i < segments.size(); ++i) {
const float w = segments[i].lineWidth > 0 ? segments[i].lineWidth : 2.0f;
widthGroups[w].append(i);
}
for (const auto& group : widthGroups) {
QVector<float> positions;
QVector<float> colors;
positions.reserve(group.second.size() * 6);
colors.reserve(group.second.size() * 6);
for (int idx : group.second) {
const LineSegment& seg = segments[idx];
positions.append(seg.x1); positions.append(seg.y1); positions.append(seg.z1);
positions.append(seg.x2); positions.append(seg.y2); positions.append(seg.z2);
colors.append(seg.r); colors.append(seg.g); colors.append(seg.b);
colors.append(seg.r); colors.append(seg.g); colors.append(seg.b);
}
if (positions.isEmpty()) {
continue;
}
// GLES2 下 glLineWidth 有效宽度常受限(部分 Mali 仅支持 1.0),
// 仍按原线宽设置,驱动不支持时自动退化为最接近的可用宽度。
glLineWidth(group.first);
drawPrimitives(m_mvp, positions.constData(), positions.size() / 3,
GL_LINES, colors.constData(),
QVector3D(1.0f, 1.0f, 1.0f), 1.0f, 1.0f, true);
}
glLineWidth(1.0f);
}
void PointCloudGLWidget::drawPosePoints()
{
if (m_posePoints.isEmpty())
{
return;
}
// 从视图矩阵取相机右/上方向,把轴线拉成面向屏幕的窄带,避免 GLES2 线宽被钳成 1 像素。
auto applyEulerRotation = [this](QMatrix4x4& poseLocal, const PosePoint& pose)
{
switch (m_eulerRotationOrder)
{
case EulerRotationOrder::ZYX:
poseLocal.rotate(pose.rx, 1, 0, 0);
poseLocal.rotate(pose.ry, 0, 1, 0);
poseLocal.rotate(pose.rz, 0, 0, 1);
break;
case EulerRotationOrder::XYZ:
poseLocal.rotate(pose.rz, 0, 0, 1);
poseLocal.rotate(pose.ry, 0, 1, 0);
poseLocal.rotate(pose.rx, 1, 0, 0);
break;
case EulerRotationOrder::ZXY:
poseLocal.rotate(pose.ry, 0, 1, 0);
poseLocal.rotate(pose.rx, 1, 0, 0);
poseLocal.rotate(pose.rz, 0, 0, 1);
break;
case EulerRotationOrder::YXZ:
poseLocal.rotate(pose.rz, 0, 0, 1);
poseLocal.rotate(pose.rx, 1, 0, 0);
poseLocal.rotate(pose.ry, 0, 1, 0);
break;
case EulerRotationOrder::XZY:
poseLocal.rotate(pose.ry, 0, 1, 0);
poseLocal.rotate(pose.rz, 0, 0, 1);
poseLocal.rotate(pose.rx, 1, 0, 0);
break;
case EulerRotationOrder::YZX:
poseLocal.rotate(pose.rx, 1, 0, 0);
poseLocal.rotate(pose.rz, 0, 0, 1);
poseLocal.rotate(pose.ry, 0, 1, 0);
break;
}
};
QVector<float> positions;
QVector<float> colors;
positions.reserve(m_posePoints.size() * 18);
colors.reserve(m_posePoints.size() * 18);
const auto appendAxis = [&](const QVector3D& start,
const QVector3D& end,
const QVector3D& color)
{
positions.append(start.x());
positions.append(start.y());
positions.append(start.z());
positions.append(end.x());
positions.append(end.y());
positions.append(end.z());
for (int i = 0; i < 2; ++i)
{
colors.append(color.x());
colors.append(color.y());
colors.append(color.z());
}
};
for (const PosePoint& pose : m_posePoints)
{
QMatrix4x4 poseLocal;
poseLocal.setToIdentity();
poseLocal.translate(pose.x, pose.y, pose.z);
applyEulerRotation(poseLocal, pose);
const float axisLength = qMax(1.0f, pose.scale);
const QVector3D origin = poseLocal.map(QVector3D(0.0f, 0.0f, 0.0f));
const QVector3D xEnd = poseLocal.map(QVector3D(axisLength, 0.0f, 0.0f));
const QVector3D yEnd = poseLocal.map(QVector3D(0.0f, axisLength, 0.0f));
const QVector3D zEnd = poseLocal.map(QVector3D(0.0f, 0.0f, axisLength));
appendAxis(origin, xEnd, QVector3D(1.0f, 0.0f, 0.0f));
appendAxis(origin, yEnd, QVector3D(0.0f, 1.0f, 0.0f));
appendAxis(origin, zEnd, QVector3D(0.0f, 0.0f, 1.0f));
}
drawPrimitives(m_mvp,
positions.constData(),
positions.size() / 3,
GL_LINES,
colors.constData(),
QVector3D(1.0f, 1.0f, 1.0f),
1.0f,
1.0f,
false);
}