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

2024-03-03 16:40

本文主要是介绍Qt使用QPainter绘制一个3D立方体,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1.实现思路

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

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

    //立方体前后四个顶点,从右上角开始顺时针vertexArr=QVector<QVector3D>{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<QVector<int>>{{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 <QWidget>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QVector3D>
#include <QMatrix4x4>
#include <QQuaternion>//绘制一个立方体盒子
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<QVector3D> vertexArr;//立方体六个面QVector<QVector<int>> elementArr;//观察矩阵旋转QVector3D rotationAxis;QQuaternion rotationQuat;//透视投影的fovy参数,视野范围float projectionFovy{30.0f};//鼠标位置QPoint mousePos;//鼠标按下标志位bool mousePressed{false};
};
#include "Cube3D.h"#include <QPainter>
#include <QPainterPath>
#include <QtMath>
#include <QDebug>Cube3D::Cube3D(QWidget *parent) : QWidget(parent)
{//          7------------------4//        /                 /  |//     3------------------0    |//     |                  |    |//     |                  |    |//     |                  |    |//     |                  |    |//     |    6             |    5//     |                  |  ///     2------------------1//立方体前后四个顶点,从右上角开始顺时针vertexArr = QVector<QVector3D>{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<QVector<int>>{{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<QVector3D> vertex_list; //和矩阵运算后的顶点QList<int> 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<QPainterPath> element_path_list; //每个面路径QList<float> element_z_values;         //每个面中心点的z值QList<QPointF> element_z_points;       //每个面中心点在平面对应xy值QList<int> 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);//对角线中间点作为面的zelement_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<int> 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 outprojectionFovy += 0.5f;if (projectionFovy > 90)projectionFovy = 90;}else{//鼠标向上滑动为+,这里作为zoom inprojectionFovy -= 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};
}

这篇关于Qt使用QPainter绘制一个3D立方体的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/770240

相关文章

Pandas使用SQLite3实战

《Pandas使用SQLite3实战》本文主要介绍了Pandas使用SQLite3实战,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录1 环境准备2 从 SQLite3VlfrWQzgt 读取数据到 DataFrame基础用法:读

JSON Web Token在登陆中的使用过程

《JSONWebToken在登陆中的使用过程》:本文主要介绍JSONWebToken在登陆中的使用过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录JWT 介绍微服务架构中的 JWT 使用结合微服务网关的 JWT 验证1. 用户登录,生成 JWT2. 自定义过滤

Java中StopWatch的使用示例详解

《Java中StopWatch的使用示例详解》stopWatch是org.springframework.util包下的一个工具类,使用它可直观的输出代码执行耗时,以及执行时间百分比,这篇文章主要介绍... 目录stopWatch 是org.springframework.util 包下的一个工具类,使用它

Java使用Curator进行ZooKeeper操作的详细教程

《Java使用Curator进行ZooKeeper操作的详细教程》ApacheCurator是一个基于ZooKeeper的Java客户端库,它极大地简化了使用ZooKeeper的开发工作,在分布式系统... 目录1、简述2、核心功能2.1 CuratorFramework2.2 Recipes3、示例实践3

springboot security使用jwt认证方式

《springbootsecurity使用jwt认证方式》:本文主要介绍springbootsecurity使用jwt认证方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录前言代码示例依赖定义mapper定义用户信息的实体beansecurity相关的类提供登录接口测试提供一

go中空接口的具体使用

《go中空接口的具体使用》空接口是一种特殊的接口类型,它不包含任何方法,本文主要介绍了go中空接口的具体使用,具有一定的参考价值,感兴趣的可以了解一下... 目录接口-空接口1. 什么是空接口?2. 如何使用空接口?第一,第二,第三,3. 空接口几个要注意的坑坑1:坑2:坑3:接口-空接口1. 什么是空接

springboot security快速使用示例详解

《springbootsecurity快速使用示例详解》:本文主要介绍springbootsecurity快速使用示例,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝... 目录创www.chinasem.cn建spring boot项目生成脚手架配置依赖接口示例代码项目结构启用s

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

java中使用POI生成Excel并导出过程

《java中使用POI生成Excel并导出过程》:本文主要介绍java中使用POI生成Excel并导出过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录需求说明及实现方式需求完成通用代码版本1版本2结果展示type参数为atype参数为b总结注:本文章中代码均为

Spring Boot3虚拟线程的使用步骤详解

《SpringBoot3虚拟线程的使用步骤详解》虚拟线程是Java19中引入的一个新特性,旨在通过简化线程管理来提升应用程序的并发性能,:本文主要介绍SpringBoot3虚拟线程的使用步骤,... 目录问题根源分析解决方案验证验证实验实验1:未启用keep-alive实验2:启用keep-alive扩展建