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

相关文章

C#中checked关键字的使用小结

《C#中checked关键字的使用小结》本文主要介绍了C#中checked关键字的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录✅ 为什么需要checked? 问题:整数溢出是“静默China编程”的(默认)checked的三种用

C#中预处理器指令的使用小结

《C#中预处理器指令的使用小结》本文主要介绍了C#中预处理器指令的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录 第 1 名:#if/#else/#elif/#endif✅用途:条件编译(绝对最常用!) 典型场景: 示例

Mysql中RelayLog中继日志的使用

《Mysql中RelayLog中继日志的使用》MySQLRelayLog中继日志是主从复制架构中的核心组件,负责将从主库获取的Binlog事件暂存并应用到从库,本文就来详细的介绍一下RelayLog中... 目录一、什么是 Relay Log(中继日志)二、Relay Log 的工作流程三、Relay Lo

使用Redis实现会话管理的示例代码

《使用Redis实现会话管理的示例代码》文章介绍了如何使用Redis实现会话管理,包括会话的创建、读取、更新和删除操作,通过设置会话超时时间并重置,可以确保会话在用户持续活动期间不会过期,此外,展示了... 目录1. 会话管理的基本概念2. 使用Redis实现会话管理2.1 引入依赖2.2 会话管理基本操作

Springboot请求和响应相关注解及使用场景分析

《Springboot请求和响应相关注解及使用场景分析》本文介绍了SpringBoot中用于处理HTTP请求和构建HTTP响应的常用注解,包括@RequestMapping、@RequestParam... 目录1. 请求处理注解@RequestMapping@GetMapping, @PostMappin

springboot3.x使用@NacosValue无法获取配置信息的解决过程

《springboot3.x使用@NacosValue无法获取配置信息的解决过程》在SpringBoot3.x中升级Nacos依赖后,使用@NacosValue无法动态获取配置,通过引入SpringC... 目录一、python问题描述二、解决方案总结一、问题描述springboot从2android.x

SpringBoot整合AOP及使用案例实战

《SpringBoot整合AOP及使用案例实战》本文详细介绍了SpringAOP中的切入点表达式,重点讲解了execution表达式的语法和用法,通过案例实战,展示了AOP的基本使用、结合自定义注解以... 目录一、 引入依赖二、切入点表达式详解三、案例实战1. AOP基本使用2. AOP结合自定义注解3.

Python中Request的安装以及简单的使用方法图文教程

《Python中Request的安装以及简单的使用方法图文教程》python里的request库经常被用于进行网络爬虫,想要学习网络爬虫的同学必须得安装request这个第三方库,:本文主要介绍P... 目录1.Requests 安装cmd 窗口安装为pycharm安装在pycharm设置中为项目安装req

Qt实现对Word网页的读取功能

《Qt实现对Word网页的读取功能》文章介绍了几种在Qt中实现Word文档(.docx/.doc)读写功能的方法,包括基于QAxObject的COM接口调用、DOCX模板替换及跨平台解决方案,重点讨论... 目录1. 核心实现方式2. 基于QAxObject的COM接口调用(Windows专用)2.1 环境

使用Python将PDF表格自动提取并写入Word文档表格

《使用Python将PDF表格自动提取并写入Word文档表格》在实际办公与数据处理场景中,PDF文件里的表格往往无法直接复制到Word中,本文将介绍如何使用Python从PDF文件中提取表格数据,并将... 目录引言1. 加载 PDF 文件并准备 Word 文档2. 提取 PDF 表格并创建 Word 表格