Qt使用QPainter绘制一个3D立方体

您所在的位置:网站首页 长方体和正方体怎么画图 Qt使用QPainter绘制一个3D立方体

Qt使用QPainter绘制一个3D立方体

2024-07-08 16:41| 来源: 网络整理| 查看: 265

1.实现思路

(网上有另一篇类似的,不过他不是用的 Qt 自带的矩阵运算类:https://blog.csdn.net/BIG_C_GOD/article/details/53285152)

实现思路有点类似使用 OpenGL 画立方体,先准备顶点数据:

//立方体前后四个顶点,从右上角开始顺时针 vertexArr=QVector{ QVector3D{1,1,1}, QVector3D{1,-1,1}, QVector3D{-1,-1,1}, QVector3D{-1,1,1}, QVector3D{1,1,-1}, QVector3D{1,-1,-1}, QVector3D{-1,-1,-1}, QVector3D{-1,1,-1} }; //六个面,一个面包含四个顶点 elementArr=QVector{ {0,1,2,3}, {4,5,6,7}, {0,4,5,1}, {1,5,6,2}, {2,6,7,3}, {3,7,4,0} };

然后再和旋转矩阵、透视矩阵进行运算,得到 3D 顶点坐标在 2D 平面上的 xy 值。根据顶点 xy 值,得到每个面的路径,然后绘制表面的路径。

(2021-11-07)修复了矩阵计算错误,之前用的向量乘以矩阵,实际应该反过来。所以之前的逻辑没用透视投影也会有透视的效果,误打误撞。

这里面比较麻烦的是判断哪些是表面,单个立方体还好,可以遍历比较 z 值,如果是多个物体运算量就大了,还是直接 OpenGL 吧,毕竟我这个只是画着玩的。

2.实现代码

代码 github 链接:https://github.com/gongjianbo/EasyQPainter

实现效果 GIF 动图:

 主要代码:

#pragma once #include #include #include #include #include #include //绘制一个立方体盒子 class Cube3D : public QWidget { Q_OBJECT public: explicit Cube3D(QWidget *parent = nullptr); protected: void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; QPointF getPoint(const QVector3D &vt, int w) const; private: //立方体八个顶点 QVector vertexArr; //立方体六个面 QVector elementArr; //观察矩阵旋转 QVector3D rotationAxis; QQuaternion rotationQuat; //透视投影的fovy参数,视野范围 float projectionFovy{30.0f}; //鼠标位置 QPoint mousePos; //鼠标按下标志位 bool mousePressed{false}; }; #include "Cube3D.h" #include #include #include #include Cube3D::Cube3D(QWidget *parent) : QWidget(parent) { // 7------------------4 // / / | // 3------------------0 | // | | | // | | | // | | | // | | | // | 6 | 5 // | | / // 2------------------1 //立方体前后四个顶点,从右上角开始顺时针 vertexArr = QVector{ QVector3D{1, 1, 1}, QVector3D{1, -1, 1}, QVector3D{-1, -1, 1}, QVector3D{-1, 1, 1}, QVector3D{1, 1, -1}, QVector3D{1, -1, -1}, QVector3D{-1, -1, -1}, QVector3D{-1, 1, -1}}; //六个面,一个面包含四个顶点 elementArr = QVector{ {0, 1, 2, 3}, {4, 5, 6, 7}, {0, 4, 5, 1}, {1, 5, 6, 2}, {2, 6, 7, 3}, {3, 7, 4, 0}}; //Widget默认没有焦点,此处设置为点击时获取焦点 setFocusPolicy(Qt::ClickFocus); } void Cube3D::paintEvent(QPaintEvent *event) { Q_UNUSED(event) QPainter painter(this); //先画一个白底黑框 painter.fillRect(this->rect(), Qt::white); QPen pen(Qt::black); painter.setPen(pen); painter.drawRect(this->rect().adjusted(0, 0, -1, -1)); //右下角会超出范围 //思路,找到z值最高的顶点,然后绘制该顶点相邻的面 // 根据z值计算,近大远小 //(此外,Qt是屏幕坐标系,原点在左上角) //矩形边框参考大小 const int cube_width = (width() > height() ? height() : width()) / 4; //投影矩阵 //(之前计算错误,向量放在了矩阵左侧,误打误撞也实现了效果) QMatrix4x4 perspective_mat; perspective_mat.perspective(projectionFovy, 1.0f, 0.1f, 100.0f); //观察矩阵 QMatrix4x4 view_mat; view_mat.translate(0.0f, 0.0f, -5.0f); view_mat.rotate(rotationQuat); //计算顶点变换后坐标,包含z值max点就是正交表面可见的, //再计算下远小近大的透视投影效果齐活了 QList vertex_list; //和矩阵运算后的顶点 QList vertex_max_list; //z最大值列表(z值可能重复),内容为vertexArr的下标 float vertex_max_value; //顶点列表z最大值 //根据旋转矩阵计算每个顶点 for (int i = 0; i < vertexArr.count(); i++) { //以物体中心为原点旋转 QVector3D vertex = perspective_mat * view_mat * vertexArr.at(i); vertex.setZ(-vertex.z()); vertex.setY(-vertex.y()); vertex_list.push_back(vertex); //找出z值max的顶点 if (i == 0) { vertex_max_list.push_back(0); vertex_max_value = vertex.z(); } else { if (vertex.z() > vertex_max_value) { //找最大的z值 vertex_max_list.clear(); vertex_max_list.push_back(i); vertex_max_value = vertex.z(); } else if (abs(vertex.z() - vertex_max_value) < (1E-7)) { //和最大z值相等的也添加到列表 vertex_max_list.push_back(i); } } } //把原点移到中间来,方便绘制 painter.save(); painter.translate(width() / 2, height() / 2); //绘制front和back六个面,先计算路径再绘制 QList element_path_list; //每个面路径 QList element_z_values; //每个面中心点的z值 QList element_z_points; //每个面中心点在平面对应xy值 QList element_front_list; //elementArr中表面的index //计算每个表面 for (int i = 0; i < elementArr.count(); i++) { //每个面四个顶点 const QVector3D &vt0 = vertex_list.at(elementArr.at(i).at(0)); const QVector3D &vt1 = vertex_list.at(elementArr.at(i).at(1)); const QVector3D &vt2 = vertex_list.at(elementArr.at(i).at(2)); const QVector3D &vt3 = vertex_list.at(elementArr.at(i).at(3)); //单个面的路径,面根据大小等比放大 QPainterPath element_path; element_path.moveTo(getPoint(vt0, cube_width)); element_path.lineTo(getPoint(vt1, cube_width)); element_path.lineTo(getPoint(vt2, cube_width)); element_path.lineTo(getPoint(vt3, cube_width)); element_path.closeSubpath(); //包含zmax点的就是正交表面可见的 bool is_front = true; for (int vertex_index : vertex_max_list) { if (!elementArr.at(i).contains(vertex_index)) { is_front = false; break; } } if (is_front) { element_front_list.push_back(i); } element_path_list.push_back(element_path); //对角线中间点作为面的z element_z_values.push_back((vt0.z() + vt2.z()) / 2); //对角线中间点 element_z_points.push_back((getPoint(vt0, cube_width) + getPoint(vt2, cube_width)) / 2); } //远小近大,还要把包含max但是被近大遮盖的去掉 QList element_front_remove; for (int i = 0; i < element_front_list.count(); i++) { for (int j = 0; j < element_front_list.count(); j++) { if (i == j) continue; const int index_i = element_front_list.at(i); const int index_j = element_front_list.at(j); if (element_z_values.at(index_i) > element_z_values.at(index_j) && element_path_list.at(index_i).contains(element_z_points.at(index_j))) { element_front_remove.push_back(index_j); } } } for (int index : element_front_remove) { element_front_list.removeOne(index); } //根据计算好的路径绘制 painter.setRenderHint(QPainter::Antialiasing, true); //画表面 for (auto index : element_front_list) { painter.fillPath(element_path_list.at(index), Qt::green); } //画被遮盖面的边框虚线 painter.setPen(QPen(Qt::white, 1, Qt::DashLine)); for (int i = 0; i < element_path_list.count(); i++) { if (element_front_list.contains(i)) continue; painter.drawPath(element_path_list.at(i)); } //画表面边框 painter.setPen(QPen(Qt::black, 2)); for (auto index : element_front_list) { painter.drawPath(element_path_list.at(index)); } painter.restore(); painter.drawText(20, 30, "Drag Moving"); } void Cube3D::mousePressEvent(QMouseEvent *event) { mousePressed = true; mousePos = event->pos(); QWidget::mousePressEvent(event); } void Cube3D::mouseMoveEvent(QMouseEvent *event) { if (mousePressed) { QVector2D diff = QVector2D(event->pos()) - QVector2D(mousePos); mousePos = event->pos(); QVector3D n = QVector3D(diff.y(), diff.x(), 0.0).normalized(); rotationAxis = (rotationAxis + n).normalized(); //不能对换乘的顺序 rotationQuat = QQuaternion::fromAxisAndAngle(rotationAxis, 2.0f) * rotationQuat; update(); } QWidget::mouseMoveEvent(event); } void Cube3D::mouseReleaseEvent(QMouseEvent *event) { mousePressed = false; QWidget::mouseReleaseEvent(event); } void Cube3D::wheelEvent(QWheelEvent *event) { event->accept(); //fovy越小,模型看起来越大 if (event->delta() < 0) { //鼠标向下滑动为-,这里作为zoom out projectionFovy += 0.5f; if (projectionFovy > 90) projectionFovy = 90; } else { //鼠标向上滑动为+,这里作为zoom in projectionFovy -= 0.5f; if (projectionFovy < 1) projectionFovy = 1; } update(); } QPointF Cube3D::getPoint(const QVector3D &vt, int w) const { //可以用z来手动计算远小近大,也可以矩阵运算 //const float z_offset=vt.z()*0.1; //return QPointF{ vt.x()*w*(1+z_offset), vt.y()*w*(1+z_offset) }; return QPointF{vt.x() * w, vt.y() * w}; }


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3