1155 lines
37 KiB
C++
1155 lines
37 KiB
C++
//
|
||
// Created by Lenn on 2025/12/16.
|
||
//
|
||
|
||
#include "glwidget.h"
|
||
#include <QOpenGLShaderProgram>
|
||
#include <QByteArray>
|
||
#include <QtMath>
|
||
#include <QFile>
|
||
#include <QDebug>
|
||
#include <GL/gl.h>
|
||
#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, ¢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<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 个 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);
|
||
}
|