#include "PointCloudGLWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "VrLog.h" // OpenGL/GLU 头文件 #ifdef _WIN32 #include #include #include #else #include #include #endif namespace { // 将角度归一化到 [-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; } } PointCloudGLWidget::PointCloudGLWidget(QWidget* parent) : 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) , m_lineSelectMode(LineSelectMode::Vertical) , m_eulerRotationOrder(EulerRotationOrder::ZYX) , m_measureDistanceEnabled(false) , m_hasListHighlightPoint(false) , m_colorIndex(0) , m_viewToolbar(nullptr) , m_viewAngleButton(nullptr) , m_zoomInButton(nullptr) , m_zoomOutButton(nullptr) { setFocusPolicy(Qt::StrongFocus); setMouseTracking(true); setAttribute(Qt::WA_AcceptTouchEvents, true); grabGesture(Qt::PinchGesture); // 确保 m_model 是单位矩阵 m_model.setToIdentity(); createOverlayControls(); connect(this, &PointCloudGLWidget::viewAnglesChanged, this, [this](float, float, float) { updateViewAngleDisplay(); }); } bool PointCloudGLWidget::event(QEvent* event) { if (event->type() == QEvent::Gesture) { auto* gestureEvent = static_cast(event); if (auto* pinch = static_cast( 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(scale))) { return false; } m_distance = qBound(minimumCameraDistance(), m_pinchStartDistance / static_cast(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(data.vertices.size() * sizeof(float))); data.vertexBuffer.release(); // 如果有颜色数据,创建并上传颜色 VBO if (data.hasColor && !data.colors.empty()) { data.colorBuffer.create(); data.colorBuffer.bind(); data.colorBuffer.setUsagePattern(QOpenGLBuffer::StaticDraw); data.colorBuffer.allocate(data.colors.data(), static_cast(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::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->show(); // ── 左下角旋转角度显示按钮(点击弹窗输入角度) ── 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->show(); // ── 右侧悬浮缩放按钮(垂直居中,与 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->show(); 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->show(); updateViewAngleDisplay(); repositionOverlayControls(); } void PointCloudGLWidget::repositionOverlayControls() { const int widgetWidth = width(); const int widgetHeight = height(); if (widgetWidth <= 0 || widgetHeight <= 0) { return; } // 左侧视图工具栏:垂直居中 if (m_viewToolbar) { m_viewToolbar->move(8, qMax(0, (widgetHeight - m_viewToolbar->height()) / 2)); } // 左下角旋转角度按钮 if (m_viewAngleButton) { m_viewAngleButton->adjustSize(); const int margin = 8; m_viewAngleButton->move(margin, widgetHeight - m_viewAngleButton->height() - margin); } // 右侧缩放按钮:垂直居中 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 = 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); } } void PointCloudGLWidget::resizeEvent(QResizeEvent* event) { QOpenGLWidget::resizeEvent(event); repositionOverlayControls(); } 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(spinX->value()), static_cast(spinY->value()), static_cast(spinZ->value())); } } void PointCloudGLWidget::initializeGL() { initializeOpenGLFunctions(); LOG_INFO("[CloudView] OpenGL initialized, version: %s\n", reinterpret_cast(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()) { #ifdef GL_PROGRAM_POINT_SIZE glEnable(GL_PROGRAM_POINT_SIZE); #endif } if (!m_dynamicVbo.isCreated()) { m_dynamicVbo.create(); } m_shaderReady = false; ensureShader(); } bool PointCloudGLWidget::ensureShader() { if (m_shaderReady) { return m_shader != nullptr; } m_shaderReady = true; 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")) { LOG_ERROR("[CloudView] Shader compile failed: %s\n", program->log().isEmpty() ? "unknown error" : qPrintable(program->log())); delete program; return false; } if (!program->link()) { LOG_ERROR("[CloudView] Shader link failed: %s\n", program->log().isEmpty() ? "unknown error" : qPrintable(program->log())); delete program; 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"); } return m_shader != nullptr; } 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); // 兜底:桌面 GL 未开启 GL_PROGRAM_POINT_SIZE 时,点大小取自 GL_POINT_SIZE 状态 glPointSize(pointSize); data.vertexBuffer.bind(); glEnableVertexAttribArray(m_positionLocation); glVertexAttribPointer(m_positionLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr); const bool useVertexColor = 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(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); // 兜底:桌面 GL 未开启 GL_PROGRAM_POINT_SIZE 时,点大小取自 GL_POINT_SIZE 状态 glPointSize(pointSize); if (depthTest) { glEnable(GL_DEPTH_TEST); } else { glDisable(GL_DEPTH_TEST); } const GLsizeiptr positionBytes = static_cast(vertexCount) * 3 * sizeof(float); const GLsizeiptr colorBytes = colors ? static_cast(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(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); } 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); // 调试输出:每100帧输出一次当前旋转角度 static int frameCount = 0; if (++frameCount % 100 == 0) { // 从四元数转换为欧拉角用于显示 QVector3D euler = m_rotation.toEulerAngles(); LOG_INFO("[CloudView] Current rotation: pitch=%.1f, yaw=%.1f, roll=%.1f\n", euler.x(), euler.y(), euler.z()); } QMatrix4x4 modelView = m_view * m_model; updateProjection(modelView); m_mvp = m_projection * modelView; 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, m_pointSize); // 绘制自定义大小的点(RGBA 中 A > 1 的点) if (cloudData.hasCustomPointSizes) { glDepthFunc(GL_LEQUAL); // 允许在相同深度覆盖绘制 const bool cloudHasColor = cloudData.colorBuffer.isCreated() && !cloudData.colors.empty(); for (const auto& group : cloudData.customPointSizeGroups) { QVector positions; positions.reserve(static_cast(group.indices.size()) * 3); QVector colors; if (cloudHasColor) { colors.reserve(static_cast(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(); } void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString& name) { LOG_INFO("[CloudView] addPointCloud called, cloud size: %zu\n", cloud.size()); if (cloud.empty()) { 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; 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; } 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); // 保存原始索引 data.originalIndices.push_back(static_cast(i)); // 保存线内索引(点在所属线中的位置) data.pointInLineIndices.push_back(ptInLineCounter); // 保存线索引 if (lineIdx >= 0) { data.lineIndices.push_back(lineIdx); if (lineIdx + 1 > data.totalLines) { data.totalLines = lineIdx + 1; } } ptInLineCounter++; } // 计算每线点数(假设网格化点云) if (data.totalLines > 0) { data.pointsPerLine = static_cast(cloud.size()) / data.totalLines; } LOG_INFO("[CloudView] Valid vertices count: %zu, colorIndex: %d, totalLines: %d, pointsPerLine: %d\n", data.vertices.size() / 3, data.colorIndex, data.totalLines, data.pointsPerLine); m_pointClouds.push_back(std::move(data)); // 上传 VBO(如果当前在 GL 上下文中) makeCurrent(); uploadToVBO(m_pointClouds.back()); doneCurrent(); computeBoundingBox(); LOG_INFO("[CloudView] BoundingBox min: (%.3f, %.3f, %.3f) max: (%.3f, %.3f, %.3f)\n", m_minBound.x(), m_minBound.y(), m_minBound.z(), m_maxBound.x(), m_maxBound.y(), m_maxBound.z()); LOG_INFO("[CloudView] Center: (%.3f, %.3f, %.3f) Distance: %.3f\n", m_center.x(), m_center.y(), m_center.z(), m_distance); resetView(); update(); } void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QString& name) { 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> sizeGroupMap; const float EPSILON = 1e-6f; int prevLineIdx = -1; int ptInLineCounter = 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; } 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); 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(i)); // 保存线内索引(点在所属线中的位置) data.pointInLineIndices.push_back(ptInLineCounter); // 保存线索引 if (lineIdx >= 0) { data.lineIndices.push_back(lineIdx); if (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)); } LOG_INFO("[CloudView] Found %zu custom point size groups\n", data.customPointSizeGroups.size()); } // 计算每线点数 if (data.totalLines > 0) { data.pointsPerLine = static_cast(cloud.size()) / data.totalLines; } m_pointClouds.push_back(std::move(data)); // 上传 VBO makeCurrent(); uploadToVBO(m_pointClouds.back()); doneCurrent(); computeBoundingBox(); resetView(); 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(); data.hasLineInfo = data.hasLineInfo || incomingHasLineInfo; const size_t originalIndexOffset = data.vertices.size() / 3; const float epsilon = 1e-6f; int previousLineIndex = -1; int pointInLineIndex = 0; 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; } 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); 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(originalIndexOffset + index)); data.pointInLineIndices.push_back(pointInLineIndex); if (lineIndex >= 0) { data.lineIndices.push_back(lineIndex); 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 = static_cast(data.vertices.size() / 3) / data.totalLines; } makeCurrent(); uploadToVBO(data); doneCurrent(); computeBoundingBox(); m_distance = qMax(m_distance, fittedCameraDistance()); update(); } void PointCloudGLWidget::clearPointClouds() { // 释放所有 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(); } void PointCloudGLWidget::setPointCloudColor(PointCloudColor color) { m_currentColor = color; update(); } void PointCloudGLWidget::setPointSize(float size) { m_pointSize = qBound(1.0f, size, 10.0f); update(); } 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(); 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(); 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; if (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); } const float aspect = static_cast(width()) / static_cast(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(width()) / static_cast(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()); 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; } // 设置选中的横向线 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; 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; LOG_INFO("[CloudView] Set selected point %d to (%.3f, %.3f, %.3f)\n", index, x, y, z); // 刷新显示 update(); } QVector PointCloudGLWidget::getSelectedLinePoints() const { QVector points; if (!m_selectedLine.valid || m_selectedLine.cloudIndex < 0) { return points; } if (m_selectedLine.cloudIndex >= static_cast(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.originalIndices.size(); ++i) { int originalIdx = cloudData.originalIndices[i]; if (originalIdx % cloudData.pointsPerLine == 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(); } } // 重新上传 VBO(顶点数据已变更) makeCurrent(); for (auto& cloudData : m_pointClouds) { uploadToVBO(cloudData); } doneCurrent(); computeBoundingBox(); 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; } } } if (rotatedCount > 0) { makeCurrent(); for (auto& cloudData : m_pointClouds) { uploadToVBO(cloudData); } doneCurrent(); computeBoundingBox(); resetView(); update(); } return rotatedCount; } size_t PointCloudGLWidget::getAllCloudsByLines(std::vector>& 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 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(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(lines) * ptsPerLine; } else { // 无线信息,每个点云作为一条线 std::vector line; line.reserve(displayCount); for (size_t i = 0; i < displayCount; ++i) { SVzNL3DPosition pos; memset(&pos, 0, sizeof(pos)); pos.nPointIdx = static_cast(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) { for (size_t i = 0; i < cloudData.vertices.size(); i += 3) { includePoint(cloudData.vertices[i], cloudData.vertices[i + 1], cloudData.vertices[i + 2]); } } 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(width()), static_cast(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(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]; } } 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 { LOG_INFO("[CloudView] No point selected, minScreenDist=%.1f\n", std::sqrt(minScreenDist)); } return result; } void PointCloudGLWidget::drawSelectedPoints() { QVector positions; QVector 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(m_pointClouds.size())) { return; } const auto& cloudData = m_pointClouds[m_selectedLine.cloudIndex]; if (!cloudData.hasLineInfo) { return; } // 高亮显示选中线上的所有点(黄色) QVector 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.originalIndices.size(); ++i) { const int originalIdx = cloudData.originalIndices[i]; // 原始索引 % 每线点数 == 选中的点索引 if (originalIdx % cloudData.pointsPerLine == 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::mousePressEvent(QMouseEvent* event) { m_lastMousePos = event->pos(); if (event->button() == Qt::LeftButton) { // Ctrl+左键:选点 if (event->modifiers() & Qt::ControlModifier) { 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(); } } 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(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(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) { // Alt+左键拖动:绕视线方向旋转(滚转) if (event->modifiers() & Qt::AltModifier) { // 绕Z轴(视线方向)旋转(修正方向) 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); } else { // 普通左键拖动:基于当前物体坐标系的旋转 // 鼠标水平移动 -> 绕当前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_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]; } cloud.push_back(pt, lineIdx); } 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; for (size_t i = 0; i < cloud.points.size(); ++i) { const auto& pt = cloud.points[i]; if (!std::isfinite(pt.x) || !std::isfinite(pt.y) || !std::isfinite(pt.z)) { continue; } // 显示时过滤 (0,0,0) 点 if (std::fabs(pt.x) < EPSILON && std::fabs(pt.y) < EPSILON && std::fabs(pt.z) < EPSILON) { continue; } data.vertices.push_back(pt.x); data.vertices.push_back(pt.y); data.vertices.push_back(pt.z); // 保存原始索引 data.originalIndices.push_back(static_cast(i)); if (data.hasLineInfo && i < cloud.lineIndices.size()) { int lineIdx = cloud.lineIndices[i]; data.lineIndices.push_back(lineIdx); if (lineIdx + 1 > data.totalLines) { data.totalLines = lineIdx + 1; } } } // 计算每线点数 if (data.totalLines > 0) { data.pointsPerLine = static_cast(cloud.size()) / data.totalLines; } // 释放旧的 VBO makeCurrent(); releaseVBO(m_pointClouds[0]); m_pointClouds[0] = std::move(data); // 上传新的 VBO uploadToVBO(m_pointClouds[0]); doneCurrent(); computeBoundingBox(); resetView(); update(); } void PointCloudGLWidget::addLineSegments(const QVector& segments) { m_lineSegments.append(segments); update(); } void PointCloudGLWidget::clearLineSegments() { m_lineSegments.clear(); update(); } void PointCloudGLWidget::setBasicShapeSegments(const QVector& segments) { m_basicShapeSegments = segments; update(); } void PointCloudGLWidget::appendBasicShapeSegments(const QVector& segments) { if (segments.isEmpty()) { return; } for (const LineSegment& segment : segments) { m_basicShapeSegments.append(segment); } update(); } void PointCloudGLWidget::appendBasicShapeSurfaces(const QVector& surfaces) { if (surfaces.isEmpty()) { return; } for (const SurfaceQuad& surface : surfaces) { m_basicShapeSurfaces.append(surface); } update(); } void PointCloudGLWidget::appendBasicShapePoints(const QVector& 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& poses) { m_posePoints.append(poses); 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& segments) { if (segments.isEmpty()) { return; } // 按线宽分组绘制 // 收集所有不同的线宽值(0 表示默认 2.0f) std::map> 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 positions; QVector 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; } glLineWidth(2.0f); for (const auto& pose : m_posePoints) { // 移动到姿态点位置,并按欧拉角顺序旋转(QMatrix4x4::rotate 与 glRotatef 同为右乘) QMatrix4x4 modelView; modelView.setToIdentity(); modelView.translate(pose.x, pose.y, pose.z); switch (m_eulerRotationOrder) { case EulerRotationOrder::ZYX: // Yaw-Pitch-Roll(最常用) modelView.rotate(pose.rx, 1, 0, 0); modelView.rotate(pose.ry, 0, 1, 0); modelView.rotate(pose.rz, 0, 0, 1); break; case EulerRotationOrder::XYZ: // Roll-Pitch-Yaw modelView.rotate(pose.rz, 0, 0, 1); modelView.rotate(pose.ry, 0, 1, 0); modelView.rotate(pose.rx, 1, 0, 0); break; case EulerRotationOrder::ZXY: // Yaw-Roll-Pitch modelView.rotate(pose.ry, 0, 1, 0); modelView.rotate(pose.rx, 1, 0, 0); modelView.rotate(pose.rz, 0, 0, 1); break; case EulerRotationOrder::YXZ: // Pitch-Roll-Yaw modelView.rotate(pose.rz, 0, 0, 1); modelView.rotate(pose.rx, 1, 0, 0); modelView.rotate(pose.ry, 0, 1, 0); break; case EulerRotationOrder::XZY: // Roll-Yaw-Pitch modelView.rotate(pose.ry, 0, 1, 0); modelView.rotate(pose.rz, 0, 0, 1); modelView.rotate(pose.rx, 1, 0, 0); break; case EulerRotationOrder::YZX: // Pitch-Yaw-Roll modelView.rotate(pose.rx, 1, 0, 0); modelView.rotate(pose.rz, 0, 0, 1); modelView.rotate(pose.ry, 0, 1, 0); break; } const QMatrix4x4 mvp = m_projection * modelView; // 绘制坐标系(右手坐标系:X红右,Y绿上,Z蓝前) const float positions[] = { 0.0f, 0.0f, 0.0f, pose.scale, 0.0f, 0.0f, // X轴 - 红色 0.0f, 0.0f, 0.0f, 0.0f, pose.scale, 0.0f, // Y轴 - 绿色 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, pose.scale // 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, true); } glLineWidth(1.0f); }