Files
tactileipc3d/src/glwidget.cpp
2026-01-20 11:13:31 +08:00

1154 lines
37 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

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

//
// Created by Lenn on 2025/12/16.
//
#include "glwidget.h"
#include <QOpenGLShaderProgram>
#include <QByteArray>
#include <QtMath>
#include <QFile>
#include <QDebug>
#include <qevent.h>
#include <qlogging.h>
#include <qminmax.h>
#include <qopenglext.h>
#include <qopenglshaderprogram.h>
#include <qstringliteral.h>
#include <qvectornd.h>
#include <QVector3D>
#include <QVector2D>
#include <QVector4D>
#include <QImage>
#include <QPainter>
#include <QFontMetrics>
#include <algorithm>
#include <limits>
// 读取文本文件内容(这里主要用来从 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<float> &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<float> 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<float>::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<float>::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, &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(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 里只有一个单位 quadinstance 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 个 floatoffsetX, 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<float> 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<float> inst;
// 每个点 3 个 floatoffsetX, 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.01far=足够大(保证 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);
}