// // Created by Lenn on 2025/12/16. // #include "glwidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // 读取文本文件内容(这里主要用来从 Qt Resource `:/shaders/...` 读取 shader 源码) static QByteArray readFile(const QString& path) { QFile f(path); if (!f.open(QIODevice::ReadOnly)) return {}; return f.readAll(); } // 简单的 4x4 单位矩阵(目前只用于保留的 float[16] 版本) static void matIdentity(float m[16]) { for (int i = 0; i < 16; i++) { m[i] = 0; } m[0] = m[5] = m[10] = m[15] = 1; } static QVector3D toColorVec(const QColor& color) { return QVector3D(color.redF(), color.greenF(), color.blueF()); } GLWidget::GLWidget(QWidget *parent) : QOpenGLWidget(parent) { setMinimumSize(640, 480); } GLWidget::~GLWidget() { makeCurrent(); delete m_bgProg; delete m_panelProg; delete m_dotsProg; if (m_panelIbo) glDeleteBuffers(1, &m_panelIbo); if (m_panelVbo) glDeleteBuffers(1, &m_panelVbo); if (m_panelVao) glDeleteVertexArrays(1, &m_panelVao); if (m_dotsVao) glDeleteVertexArrays(1, &m_dotsVao); if (m_dotsVbo) glDeleteBuffers(1, &m_dotsVbo); if (m_instanceVbo) glDeleteBuffers(1, &m_instanceVbo); if (m_bgVbo) glDeleteBuffers(1, &m_bgVbo); if (m_bgVao) glDeleteVertexArrays(1, &m_bgVao); if (m_dotTex) glDeleteTextures(1, &m_dotTex); doneCurrent(); } void GLWidget::setPanelSize(float w, float h, float d) { m_panelW = w; m_panelH = h; m_panelD = d; m_panelGeometryDirty = true; update(); } void GLWidget::setPanelThickness(float h) { if (qFuzzyCompare(m_panelH, h)) return; m_panelD = h; m_panelGeometryDirty = true; update(); } void GLWidget::setSpec(int rows, int cols, float pitch, float dotRaius) { m_rows = qMax(0, rows); m_cols = qMax(0, cols); m_pitch = qMax(0.0f, pitch); m_dotRadius = qMax(0.0f, dotRaius); // 自动根据点阵尺寸调整 panel 的尺寸(保证圆点不会越过顶面边界)。 // 约定:点中心覆盖的范围是 (cols-1)*pitch / (rows-1)*pitch,面板需要额外留出 dotRadius 的边缘空间。 const float gridW = float(qMax(0, m_cols - 1)) * m_pitch; const float gridH = float(qMax(0, m_rows - 1)) * m_pitch; m_panelW = gridW + 2.0f * m_dotRadius; m_panelH = gridH + 2.0f * m_dotRadius; m_panelGeometryDirty = true; m_dotsGeometryDirty = true; QMutexLocker lk(&m_dataMutex); m_latestValues.resize(dotCount()); for (int i = 0; i < m_latestValues.size(); ++i) { m_latestValues[i] = m_min; } m_valuesDirty = true; m_hasData = false; update(); } void GLWidget::submitValues(const QVector &values) { if (values.size() != dotCount()) return; { QMutexLocker lk(&m_dataMutex); m_latestValues = values; m_valuesDirty = true; m_hasData = true; } update(); } void GLWidget::setRange(int minV, int maxV) { m_min = minV; m_max = maxV; update(); } void GLWidget::setYaw(float yawDeg) { if (qFuzzyCompare(m_camYawDeg, yawDeg)) return; m_camYawDeg = yawDeg; emit yawChanged(); update(); } void GLWidget::setRenderModeString(const QString &mode) { RenderMode next = DataViz; if (mode.compare(QStringLiteral("realistic"), Qt::CaseInsensitive) == 0) { next = Realistic; } else if (mode.compare(QStringLiteral("dataViz"), Qt::CaseInsensitive) == 0) { next = DataViz; } if (m_renderMode == next) return; m_renderMode = next; update(); } void GLWidget::setLabelModeString(const QString &mode) { LabelMode next = LabelsOff; if (mode.compare(QStringLiteral("hover"), Qt::CaseInsensitive) == 0) { next = LabelsHover; } else if (mode.compare(QStringLiteral("always"), Qt::CaseInsensitive) == 0) { next = LabelsAlways; } else if (mode.compare(QStringLiteral("off"), Qt::CaseInsensitive) == 0) { next = LabelsOff; } if (m_labelMode == next) return; m_labelMode = next; if (m_labelMode == LabelsOff) m_hoveredIndex = -1; update(); } void GLWidget::setLightMode(bool on = true) { if (on == m_lightMode) { return; } m_lightMode = on; update(); } void GLWidget::setShowBg(bool on = true) { if (on == m_showBg) { return; } m_showBg = on; update(); } void GLWidget::setColorLow(const QColor& color) { const QVector3D next = toColorVec(color); if (m_colorLow == next) return; m_colorLow = next; update(); } void GLWidget::setColorMid(const QColor& color) { const QVector3D next = toColorVec(color); if (m_colorMid == next) return; m_colorMid = next; update(); } void GLWidget::setColorHigh(const QColor& color) { const QVector3D next = toColorVec(color); if (m_colorHigh == next) return; m_colorHigh = next; update(); } void GLWidget::initializeGL() { initializeOpenGLFunctions(); // 基础状态:开启深度测试,否则 3D 物体的遮挡关系会不正确 glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); initPrograms_(); initDotTexture_(); initBackgroundGeometry_(); initPanelGeometry_(); initDotGeometry_(); initRoomGeometry_(); m_panelGeometryDirty = false; m_dotsGeometryDirty = false; matIdentity(m_modelPanel); matIdentity(m_view); matIdentity(m_proj); } void GLWidget::initGeometry_() { initDotTexture_(); initBackgroundGeometry_(); initPanelGeometry_(); initDotGeometry_(); initRoomGeometry_(); } void GLWidget::resizeGL(int w, int h) { glViewport(0, 0, w, h); } void GLWidget::paintGL() { glClearColor(1.0f, 1.0f, 1.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 如果点阵规格/面板尺寸发生变化,需要在有 GL 上下文时重建几何体 buffer。 if (m_panelGeometryDirty) { initPanelGeometry_(); m_panelGeometryDirty = false; } if (m_dotsGeometryDirty) { initDotGeometry_(); m_dotsGeometryDirty = false; m_valuesDirty = true; // instanceVBO 重新分配后需要重新上传数据 } // --- 背景:屏幕空间网格(不随相机旋转)--- if (m_bgProg && m_bgVao && m_showBg) { const float dpr = devicePixelRatioF(); const QVector2D viewport(float(width()) * dpr, float(height()) * dpr); glDisable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); m_bgProg->bind(); m_bgProg->setUniformValue("uViewport", viewport); m_bgProg->setUniformValue("uMinorStep", 24.0f * dpr); m_bgProg->setUniformValue("uMajorStep", 120.0f * dpr); glBindVertexArray(m_bgVao); glDrawArrays(GL_TRIANGLES, 0, 6); glBindVertexArray(0); m_bgProg->release(); glDepthMask(GL_TRUE); glEnable(GL_DEPTH_TEST); } // 1) 更新相机/投影矩阵(MVP),决定如何把 3D 世界投影到屏幕 updateMatrices_(); updateRoom_(); // 2) 如果外部提交了新数据,就把 CPU 生成的 instance 数据更新到 GPU updateInstanceBufferIfNeeded_(); if (m_panelProg) { m_panelProg->bind(); // uniforms:每次 draw 前设置的一组“常量参数”(对当前 draw call 的所有顶点/片元都一致) // uMVP: Model-View-Projection 矩阵(把顶点从世界坐标 -> 裁剪空间;gl_Position 必须输出裁剪空间坐标) m_panelProg->setUniformValue("uMVP", m_mvp); m_panelProg->setUniformValue("uCameraPos", m_cameraPos); m_panelProg->setUniformValue("uPanelW", m_panelW); m_panelProg->setUniformValue("uPanelH", m_panelH); m_panelProg->setUniformValue("uPanelD", m_panelD); m_panelProg->setUniformValue("uRows", m_rows); m_panelProg->setUniformValue("uCols", m_cols); m_panelProg->setUniformValue("uPitch", m_pitch); m_panelProg->setUniformValue("uDotRadius", m_dotRadius); m_panelProg->setUniformValue("uRenderMode", 1); m_panelProg->setUniformValue("uLightMode", m_lightMode); glBindVertexArray(m_panelVao); glDrawElements(GL_TRIANGLES, m_panelIndexCount, GL_UNSIGNED_INT, nullptr); glBindVertexArray(0); m_panelProg->release(); } if (m_dotsProg) { m_dotsProg->bind(); // uniforms:每次 draw 前设置的一组“常量参数”(对当前 draw call 的所有 instance 都一致) // uMVP: 同上;用于把每个 dot 的世界坐标变换到屏幕 m_dotsProg->setUniformValue("uMVP", m_mvp); m_dotsProg->setUniformValue("uRenderMode", 1); // uDotRadius: dot 的半径(世界坐标单位) m_dotsProg->setUniformValue("uDotRadius", m_dotRadius); // uBaseY: dot 的高度(放在 panel 顶面上方一点点,避免 z-fighting/闪烁) m_dotsProg->setUniformValue("uBaseZ", -(m_panelD * 0.5f) - 0.001f); // uMinV/uMaxV: 传感值范围,用于 fragment shader 把 value 映射成颜色 m_dotsProg->setUniformValue("uMinV", float(m_min)); m_dotsProg->setUniformValue("uMaxV", float(m_max)); m_dotsProg->setUniformValue("uColorLow", m_colorLow); m_dotsProg->setUniformValue("uColorMid", m_colorMid); m_dotsProg->setUniformValue("uColorHigh", m_colorHigh); const int hasData = (m_hasData.load() || m_dotTex == 0) ? 1 : 0; m_dotsProg->setUniformValue("uHasData", hasData); m_dotsProg->setUniformValue("uCameraPos", m_cameraPos); m_dotsProg->setUniformValue("uDotTex", 0); if (m_dotTex) { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, m_dotTex); } glBindVertexArray(m_dotsVao); glDrawArraysInstanced(GL_TRIANGLES, 0, 6, m_instanceCount); glBindVertexArray(0); if (m_dotTex) { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, 0); } // m_dotsProg->release(); } if (m_labelMode != LabelsOff && dotCount() > 0) { QVector valuesCopy; { QMutexLocker lk(&m_dataMutex); valuesCopy = m_latestValues; } auto projectToScreen = [&](const QVector3D& world, QPointF& out) -> bool { const QVector4D clip = m_mvp * QVector4D(world, 1.0f); if (clip.w() <= 1e-6f) return false; const QVector3D ndc = clip.toVector3D() / clip.w(); if (ndc.z() < -1.2f || ndc.z() > 1.2f) return false; out.setX((ndc.x() * 0.5f + 0.5f) * float(width())); out.setY((1.0f - (ndc.y() * 0.5f + 0.5f)) * float(height())); return true; }; const float baseY = (m_panelH * 0.5f) + 0.001f; const float w = (m_cols - 1) * m_pitch; const float h = (m_rows - 1) * m_pitch; QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); painter.setRenderHint(QPainter::TextAntialiasing, true); QFont font = painter.font(); font.setPixelSize(11); painter.setFont(font); const QFontMetrics fm(font); auto drawLabel = [&](const QPointF& anchor, const QPointF& offset, const QString& text) { QRectF box = fm.boundingRect(text); box.adjust(-6.0, -3.0, 6.0, 3.0); box.moveCenter(anchor + offset); const float margin = 4.0f; if (box.left() < margin) box.moveLeft(margin); if (box.top() < margin) box.moveTop(margin); if (box.right() > width() - margin) box.moveRight(width() - margin); if (box.bottom() > height() - margin) box.moveBottom(height() - margin); painter.setPen(Qt::NoPen); painter.setBrush(QColor(0, 0, 0, 150)); painter.drawRoundedRect(box, 4.0, 4.0); painter.setPen(QColor(255, 255, 255, 235)); painter.drawText(box, Qt::AlignCenter, text); }; auto dotWorldCenter = [&](int i) -> QVector3D { const int rr = (m_cols > 0) ? (i / m_cols) : 0; const int cc = (m_cols > 0) ? (i % m_cols) : 0; const float x = (cc * m_pitch) - w * 0.5f; const float z = (rr * m_pitch) - h * 0.5f; return QVector3D(x, baseY, z); }; auto dotRadiusPx = [&](const QVector3D& worldCenter, const QPointF& centerPx) -> float { QPointF edge; if (!projectToScreen(worldCenter + QVector3D(m_dotRadius, 0.0f, 0.0f), edge)) return 0.0f; const float dx = float(edge.x() - centerPx.x()); const float dy = float(edge.y() - centerPx.y()); return std::sqrt(dx * dx + dy * dy); }; if (m_labelMode == LabelsAlways && valuesCopy.size() == dotCount()) { painter.setPen(QColor(255, 255, 255, 220)); for (int i = 0; i < dotCount(); ++i) { QPointF centerPx; const QVector3D world = dotWorldCenter(i); if (!projectToScreen(world, centerPx)) continue; const float rPx = dotRadiusPx(world, centerPx); if (rPx < 12.0f) continue; const QString text = QString::number(int(valuesCopy[i] + 0.5f)); drawLabel(centerPx, QPointF(0.0, 0.0), text); } } if (m_hoveredIndex >= 0 && m_hoveredIndex < dotCount() && valuesCopy.size() == dotCount()) { QPointF centerPx; const QVector3D world = dotWorldCenter(m_hoveredIndex); if (projectToScreen(world, centerPx)) { const float rPx = dotRadiusPx(world, centerPx); painter.setBrush(Qt::NoBrush); painter.setPen(QPen(QColor(255, 255, 255, 210), 1.5)); painter.drawEllipse(centerPx, rPx * 1.08f, rPx * 1.08f); if (m_labelMode == LabelsHover) { const QString text = QString::number(int(valuesCopy[m_hoveredIndex] + 0.5f)); drawLabel(centerPx, QPointF(0.0, -rPx - 12.0f), text); } } } } } void GLWidget::mousePressEvent(QMouseEvent *event) { m_lastPos = event->pos(); if (event->button() == Qt::RightButton) { m_rightDown = true; event->accept(); return; } if (event->button() == Qt::LeftButton) { QVector3D world; const int index = pickDotIndex_(event->pos(), &world); qInfo() << "clicked index: " << index; if (index >= 0) { float value = 0.0f; int row = 0; int col = 0; if (m_cols > 0) { row = index / m_cols; col = index % m_cols; } { QMutexLocker lk(&m_dataMutex); if (index < m_latestValues.size()) value = m_latestValues[index]; } emit dotClicked(index, row, col, value); } } QOpenGLWidget::mousePressEvent(event); } void GLWidget::mouseMoveEvent(QMouseEvent *event) { const QPoint delta = event->pos() - m_lastPos; m_lastPos = event->pos(); if (m_rightDown) { m_camYawDeg += delta.x() * 0.3f; m_camPitchDeg = qBound(-89.0f, m_camPitchDeg + delta.y() * 0.3f, 89.0f); emit yawChanged(); update(); event->accept(); return; } if (m_labelMode != LabelsOff && dotCount() > 0) { auto projectToScreen = [&](const QVector3D& world, QPointF& out) -> bool { const QVector4D clip = m_mvp * QVector4D(world, 1.0f); if (clip.w() <= 1e-6f) return false; const QVector3D ndc = clip.toVector3D() / clip.w(); if (ndc.z() < -1.2f || ndc.z() > 1.2f) return false; out.setX((ndc.x() * 0.5f + 0.5f) * float(width())); out.setY((1.0f - (ndc.y() * 0.5f + 0.5f)) * float(height())); return true; }; const QPoint mousePos = event->pos(); const float baseY = (m_panelH * 0.5f) + 0.001f; const float w = (m_cols - 1) * m_pitch; const float h = (m_rows - 1) * m_pitch; int best = -1; float bestDist2 = std::numeric_limits::infinity(); for (int i = 0; i < dotCount(); ++i) { const int rr = (m_cols > 0) ? (i / m_cols) : 0; const int cc = (m_cols > 0) ? (i % m_cols) : 0; const float x = (cc * m_pitch) - w * 0.5f; const float z = (rr * m_pitch) - h * 0.5f; QPointF center; const QVector3D worldCenter(x, baseY, z); if (!projectToScreen(worldCenter, center)) continue; QPointF edge; if (!projectToScreen(worldCenter + QVector3D(m_dotRadius, 0.0f, 0.0f), edge)) continue; const float radDx = float(edge.x() - center.x()); const float radDy = float(edge.y() - center.y()); const float radPx = std::sqrt(radDx * radDx + radDy * radDy); const float threshold = radPx + 6.0f; const float dx = float(mousePos.x()) - float(center.x()); const float dy = float(mousePos.y()) - float(center.y()); const float dist2 = dx * dx + dy * dy; if (dist2 <= threshold * threshold && dist2 < bestDist2) { best = i; bestDist2 = dist2; } } if (best != m_hoveredIndex) { m_hoveredIndex = best; update(); } } QOpenGLWidget::mouseMoveEvent(event); } void GLWidget::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::RightButton) { m_rightDown = false; event->accept(); return; } QOpenGLWidget::mouseReleaseEvent(event); } void GLWidget::wheelEvent(QWheelEvent *event) { const float steps = event->angleDelta().y() / 120.0f; const float factor = std::pow(0.9f, steps); m_zoom_ = qBound(0.2f, m_zoom_ * factor, 90.0f); update(); event->accept(); } bool GLWidget::projectToScreen_(const QVector3D& world, QPointF* out) const { if (!out) return false; const QVector4D clip = m_mvp * QVector4D(world, 1.0f); if (clip.w() <= 1e-6f) return false; const QVector3D ndc = clip.toVector3D() / clip.w(); if (ndc.z() < -1.2f || ndc.z() > 1.2f) return false; out->setX((ndc.x() * 0.5f + 0.5f) * float(width())); out->setY((1.0f - (ndc.y() * 0.5f + 0.5f)) * float(height())); return true; } int GLWidget::pickDotIndex_(const QPoint& pos, QVector3D* worldOut) const { if (dotCount() <= 0) return -1; const float baseY = (m_panelH * 0.5f) + 0.001f; const float w = (m_cols - 1) * m_pitch; const float h = (m_rows - 1) * m_pitch; int best = -1; float bestDist2 = std::numeric_limits::infinity(); QVector3D bestWorld; for (int i = 0; i < dotCount(); ++i) { const int rr = (m_cols > 0) ? (i / m_cols) : 0; const int cc = (m_cols > 0) ? (i % m_cols) : 0; const QVector3D worldCenter( (cc * m_pitch) - w * 0.5f, baseY, (rr * m_pitch) - h * 0.5f ); QPointF center; if (!projectToScreen_(worldCenter, ¢er)) continue; QPointF edge; if (!projectToScreen_(worldCenter + QVector3D(m_dotRadius, 0.0f, 0.0f), &edge)) continue; const float radDx = float(edge.x() - center.x()); const float radDy = float(edge.y() - center.y()); const float radPx = std::sqrt(radDx * radDx + radDy * radDy); const float threshold = radPx + 6.0f; const float dx = float(pos.x()) - float(center.x()); const float dy = float(pos.y()) - float(center.y()); const float dist2 = dx * dx + dy * dy; if (dist2 <= threshold * threshold && dist2 < bestDist2) { best = i; bestDist2 = dist2; bestWorld = worldCenter; } } if (best >= 0 && worldOut) *worldOut = bestWorld; return best; } void GLWidget::initBackgroundGeometry_() { if (m_bgVbo) { glDeleteBuffers(1, &m_bgVbo); m_bgVbo = 0; } if (m_bgVao) { glDeleteVertexArrays(1, &m_bgVao); m_bgVao = 0; } // 全屏 quad(两个三角形),坐标直接是裁剪空间 NDC [-1,1] const float verts[] = { -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, }; glGenVertexArrays(1, &m_bgVao); glBindVertexArray(m_bgVao); glGenBuffers(1, &m_bgVbo); glBindBuffer(GL_ARRAY_BUFFER, m_bgVbo); glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0); glBindVertexArray(0); } void GLWidget::initPanelGeometry_() { // panel:目前只画“顶面矩形”,y 固定在 +panelH/2 if (m_panelIbo) { glDeleteBuffers(1, &m_panelIbo); m_panelIbo = 0; } if (m_panelVbo) { glDeleteBuffers(1, &m_panelVbo); m_panelVbo = 0; } if (m_panelVao) { glDeleteVertexArrays(1, &m_panelVao); m_panelVao = 0; } const float y = m_panelH * 0.5f; const float hw = m_panelW * 0.5; const float hd = m_panelD * 0.5f; struct V { float x, y, z; float nx, ny, nz; }; V verts[24] = { // +Y 顶面 (normal 0, +1, 0) {-hw, +y, -hd, 0, +1, 0}, // 0 {+hw, +y, -hd, 0, +1, 0}, // 1 {+hw, +y, +hd, 0, +1, 0}, // 2 {-hw, +y, +hd, 0, +1, 0}, // 3 // +Z 前面 (normal 0, 0, +1) {-hw, +y, +hd, 0, 0, +1}, // 4 {+hw, +y, +hd, 0, 0, +1}, // 5 {+hw, -y, +hd, 0, 0, +1}, // 6 {-hw, -y, +hd, 0, 0, +1}, // 7 // -Y 底面 (normal 0, -1, 0) {-hw, -y, +hd, 0, -1, 0}, // 8 {+hw, -y, +hd, 0, -1, 0}, // 9 {+hw, -y, -hd, 0, -1, 0}, // 10 {-hw, -y, -hd, 0, -1, 0}, // 11 // -Z 后面 (normal 0, 0, -1) {+hw, +y, -hd, 0, 0, -1}, // 12 {-hw, +y, -hd, 0, 0, -1}, // 13 {-hw, -y, -hd, 0, 0, -1}, // 14 {+hw, -y, -hd, 0, 0, -1}, // 15 // -X 左面 (normal -1, 0, 0) {-hw, +y, -hd, -1, 0, 0}, // 16 {-hw, +y, +hd, -1, 0, 0}, // 17 {-hw, -y, +hd, -1, 0, 0}, // 18 {-hw, -y, -hd, -1, 0, 0}, // 19 // +X 右面 (normal +1, 0, 0) {+hw, +y, +hd, +1, 0, 0}, // 20 {+hw, +y, -hd, +1, 0, 0}, // 21 {+hw, -y, -hd, +1, 0, 0}, // 22 {+hw, -y, +hd, +1, 0, 0}, // 23 }; unsigned int idx[36] = { 0, 1, 2, 0, 2, 3, // top 4, 5, 6, 4, 6, 7, // front 8, 9, 10, 8, 10,11, // bottom 12,13,14, 12,14,15, // back 16,17,18, 16,18,19, // left 20,21,22, 20,22,23 // right }; m_panelIndexCount = 36; glGenVertexArrays(1, &m_panelVao); glBindVertexArray(m_panelVao); glGenBuffers(1, &m_panelVbo); glBindBuffer(GL_ARRAY_BUFFER, m_panelVbo); glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW); glGenBuffers(1, &m_panelIbo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_panelIbo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idx), idx, GL_STATIC_DRAW); // attribute layout: // location 0: position (x,y,z) // location 1: normal (nx,ny,nz) —— 当前 shader 没用到,先留着 glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(V), (void*)0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(V), (void*)(3*sizeof(float))); glBindVertexArray(0); } void GLWidget::initDotGeometry_() { // dot:使用 instanced rendering 画很多个“圆点”(本质是一个 quad + fragment shader 里 discard 成圆) // vertex buffer 里只有一个单位 quad,instance buffer 里存每个点的位置和数值。 if (m_instanceVbo) { glDeleteBuffers(1, &m_instanceVbo); m_instanceVbo = 0; } if (m_dotsVbo) { glDeleteBuffers(1, &m_dotsVbo); m_dotsVbo = 0; } if (m_dotsVao) { glDeleteVertexArrays(1, &m_dotsVao); m_dotsVao = 0; } struct V { float x, y; float u, v; }; V quad[6] = { {-1,-1, 0,0}, { 1,-1, 1,0}, { 1, 1, 1,1}, {-1,-1, 0,0}, { 1, 1, 1,1}, {-1, 1, 0,1}, }; glGenVertexArrays(1, &m_dotsVao); glBindVertexArray(m_dotsVao); glGenBuffers(1, &m_dotsVbo); glBindBuffer(GL_ARRAY_BUFFER, m_dotsVbo); glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW); // attribute layout: // location 0: quad 顶点位置 (x,y),范围 [-1,1] // location 1: UV (u,v),用于 fragment shader 生成圆形 glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(V), (void*)0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(V), (void*)(2 * sizeof(float))); // instance buffer:每个点 3 个 float(offsetX, offsetZ, value) // layout location 2/3 对应 shader 里的 iOffsetXZ / iValue glGenBuffers(1, &m_instanceVbo); glBindBuffer(GL_ARRAY_BUFFER, m_instanceVbo); glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 3 * qMax(1, dotCount()), nullptr, GL_DYNAMIC_DRAW); // location 2: vec2 iOffsetXZ(每个 instance 一次,divisor=1) glEnableVertexAttribArray(2); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glVertexAttribDivisor(2, 1); // location 3: float iValue(每个 instance 一次,divisor=1) glEnableVertexAttribArray(3); glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(2 * sizeof(float))); glVertexAttribDivisor(3, 1); glBindVertexArray(0); } void GLWidget::initDotTexture_() { if (m_dotTex) { glDeleteTextures(1, &m_dotTex); m_dotTex = 0; } const QString path = QStringLiteral(":/images/metal.jpeg"); QImage img(path); if (img.isNull()) { qWarning() << "dot texture load failed:" << path; return; } QImage rgba = img.convertToFormat(QImage::Format_RGBA8888); glGenTextures(1, &m_dotTex); glBindTexture(GL_TEXTURE_2D, m_dotTex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA8, rgba.width(), rgba.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.constBits() ); glGenerateMipmap(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, 0); } void GLWidget::initRoomGeometry_() { qInfo() << "initRoomGeometry_()"; if (room_vao) { glDeleteVertexArrays(1, &room_vao); room_vao = 0; } if (room_vbo) { glDeleteBuffers(1, &room_vbo); room_vbo = 0; } if (room_ibo) { glDeleteBuffers(1, &room_ibo); room_ibo = 0; } using V = struct { float x, y, z; float nx, ny, nz; }; V verts[24] = { // floor (y = -1), normal +Y {-1, -1, -1, 0, +1, 0}, {+1, -1, -1, 0, +1, 0}, {+1, -1, +1, 0, +1, 0}, {-1, -1, +1, 0, +1, 0}, // ceiling (y = +1), normal -Y {-1, +1, +1, 0, -1, 0}, {+1, +1, +1, 0, -1, 0}, {+1, +1, -1, 0, -1, 0}, {-1, +1, -1, 0, -1, 0}, // back wall (z = -1), normal +Z {-1, +1, -1, 0, 0, +1}, {+1, +1, -1, 0, 0, +1}, {+1, -1, -1, 0, 0, +1}, {-1, -1, -1, 0, 0, +1}, // front wall (z = +1), normal -Z {+1, +1, +1, 0, 0, -1}, {-1, +1, +1, 0, 0, -1}, {-1, -1, +1, 0, 0, -1}, {+1, -1, +1, 0, 0, -1}, // left wall (x = -1), normal +X {-1, +1, +1, +1, 0, 0}, {-1, +1, -1, +1, 0, 0}, {-1, -1, -1, +1, 0, 0}, {-1, -1, +1, +1, 0, 0}, // right wall (x = +1), normal -X {+1, +1, -1, -1, 0, 0}, {+1, +1, +1, -1, 0, 0}, {+1, -1, +1, -1, 0, 0}, {+1, -1, -1, -1, 0, 0}, }; unsigned int idx[36] = { 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23 }; room_index_count = 36; glGenVertexArrays(1, &room_vao); glBindVertexArray(room_vao); glGenBuffers(1, &room_vbo); glBindBuffer(GL_ARRAY_BUFFER, room_vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW); glGenBuffers(1, &room_ibo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, room_ibo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idx), idx, GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(V), (void*)0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(V), (void*)(3 * sizeof(float))); glBindVertexArray(0); } void GLWidget::initPrograms_() { // Qt Resource 里打包的 shader 文件路径:使用 `:/` 前缀(不是文件系统路径) const QString vsd_path = QStringLiteral(":/shaders/dots.vert"); const QString fsd_path = QStringLiteral(":/shaders/dots.frag"); const QString vsb_path = QStringLiteral(":/shaders/bg.vert"); const QString fsb_path = QStringLiteral(":/shaders/bg.frag"); const QString vsp_path = QStringLiteral(":/shaders/panel.vert"); const QString fsp_path = QStringLiteral(":/shaders/panel.frag"); const QString vsr_path = QStringLiteral(":/shaders/room.vert"); const QString fsr_path = QStringLiteral(":/shaders/room.frag"); { auto *p = new QOpenGLShaderProgram; const bool okV = p->addShaderFromSourceCode(QOpenGLShader::Vertex, readFile(vsb_path)); const bool okF = p->addShaderFromSourceCode(QOpenGLShader::Fragment, readFile(fsb_path)); const bool okL = okV && okF && p->link(); if (!okL) { qWarning() << "bg program build failed:" << vsb_path << fsb_path << p->log(); delete p; p = nullptr; } m_bgProg = p; } { auto *p = new QOpenGLShaderProgram; const bool okV = p->addShaderFromSourceCode(QOpenGLShader::Vertex, readFile(vsp_path)); const bool okF = p->addShaderFromSourceCode(QOpenGLShader::Fragment, readFile(fsp_path)); const bool okL = okV && okF && p->link(); if (!okL) { qWarning() << "panel program build failed:" << vsp_path << fsp_path << p->log(); delete p; p = nullptr; } m_panelProg = p; } { auto *p = new QOpenGLShaderProgram; const bool okV = p->addShaderFromSourceCode(QOpenGLShader::Vertex, readFile(vsd_path)); const bool okF = p->addShaderFromSourceCode(QOpenGLShader::Fragment, readFile(fsd_path)); const bool okL = okV && okF && p->link(); if (!okL) { qWarning() << "dots program build failed:" << vsd_path << fsd_path << p->log(); delete p; p = nullptr; } m_dotsProg = p; } { auto *p = new QOpenGLShaderProgram; const bool okV = p->addShaderFromSourceCode(QOpenGLShader::Vertex, readFile(vsr_path)); const bool okF = p->addShaderFromSourceCode(QOpenGLShader::Fragment, readFile(fsr_path)); const bool okL = okV && okF && p->link(); if (!okL) { qWarning() << "dots program build failed:" << vsr_path << fsr_path << p->log(); delete p; p = nullptr; } m_roomProg = p; } } void GLWidget::updateInstanceBufferIfNeeded_() { if (dotCount() <= 0) { m_instanceCount = 0; return; } QVector valuesCopy; bool dirty = false; { QMutexLocker lk(&m_dataMutex); dirty = m_valuesDirty; if (dirty) { valuesCopy = m_latestValues; m_valuesDirty = false; } } if (!dirty) return; const int n = dotCount(); m_instanceCount = n; QVector inst; // 每个点 3 个 float:offsetX, offsetZ, value(对应 dots.vert: iOffsetXZ + iValue) inst.resize(n * 3); const float w = (m_cols - 1) * m_pitch; const float h = (m_rows - 1) * m_pitch; // 把点阵居中到原点附近(x/z 以中心对称) for (int i = 0; i < n; ++i) { const int r = (m_cols > 0) ? (i / m_cols) : 0; const int c = (m_cols > 0) ? (i % m_cols) : 0; const float x = (c * m_pitch) - w * 0.5f; const float z = (r * m_pitch) - h * 0.5f; inst[i * 3 + 0] = x; inst[i * 3 + 1] = z; inst[i * 3 + 2] = (i < valuesCopy.size()) ? valuesCopy[i] : m_min; } // 把 CPU 生成的 instance 数据上传到 GPU(后续 draw 时由 VAO 里的 attribute 2/3 读取) glBindBuffer(GL_ARRAY_BUFFER, m_instanceVbo); glBufferSubData(GL_ARRAY_BUFFER, 0, inst.size() * sizeof(float), inst.constData()); } void GLWidget::updateMatrices_() { // MVP = Projection * View * Model // // OpenGL 的顶点最终要写到 gl_Position(裁剪空间),所以我们需要一个矩阵把“世界坐标”变成“屏幕可见”的坐标。 // 这里为了入门简单: // - panel/dots 顶点数据直接当作世界坐标(Model=Identity) // - View 用 lookAt 放一个相机 // - Projection 用透视投影(perspective) const int w = width(); const int h = height(); if (w <= 0 || h <= 0) { m_mvp.setToIdentity(); return; } const float aspect = float(w) / float(h); const float radius = 0.5f * qSqrt(m_panelW * m_panelW + m_panelD * m_panelD); const float distance = qMax(0.5f, radius * 2.5f); // 让相机看向 panel 的中心点 const QVector3D center(0.0f, 0.0f, 0.0f); // yaw/pitch 控制相机绕目标点“环绕”(orbit camera) const float yawRad = qDegreesToRadians(m_camYawDeg); const float pitchRad = qDegreesToRadians(m_camPitchDeg); const float cosPitch = qCos(pitchRad); const float sinPitch = qSin(pitchRad); const QVector3D eye = center + QVector3D( distance * cosPitch * qCos(yawRad), distance * sinPitch, distance * cosPitch * qSin(yawRad) ); m_cameraPos = eye; QMatrix4x4 view; view.lookAt(eye, center, QVector3D(0.0f, 1.0f, 0.0f)); QMatrix4x4 proj; // fov=45°,near=0.01,far=足够大(保证 panel + dots 在裁剪范围内) proj.perspective(m_zoom_, aspect, 0.01f, qMax(10.0f, distance * 10.0f)); m_mvp = proj * view; } void GLWidget::updateRoom_(){ if (!m_roomProg || !room_vao || room_index_count <= 0) { return; } m_roomProg->bind(); const float base = std::max({ m_panelW, m_panelH, m_panelD }); const QVector3D roomHalfSize( std::max(1.0f, base * 1.5f), std::max(1.0f, base * 1.5f), std::max(1.0f, base * 1.5f) ); const float minorStep = std::max(0.05f, base * 0.25f); const float majorStep = minorStep * 5.0f; m_roomProg->setUniformValue("uMVP", m_mvp); m_roomProg->setUniformValue("uCameraPos", m_cameraPos); m_roomProg->setUniformValue("uRoomHalfSize", roomHalfSize); m_roomProg->setUniformValue("uMinorStep", minorStep); m_roomProg->setUniformValue("uMajorStep", majorStep); m_roomProg->setUniformValue("uRenderMode", 1); m_roomProg->setUniformValue("uLightMode", m_lightMode); m_roomProg->setUniformValue("uShowGrid", m_showGrid); glBindVertexArray(room_vao); glDrawElements(GL_TRIANGLES, room_index_count, GL_UNSIGNED_INT, nullptr); glBindVertexArray(0); m_roomProg->release(); } void GLWidget::setShowGrid(bool on) { if (m_showGrid == on) return; m_showGrid = on; update(); } void GLWidget::setRow(int row) { row = qMax(0, row); if (m_rows == row) return; setSpec(row, m_cols, m_pitch, m_dotRadius); } void GLWidget::setCol(int col) { col = qMax(0, col); if (m_cols == col) return; setSpec(m_rows, col, m_pitch, m_dotRadius); }