Qt使用QPainter绘制3D立方体

所属分类: 软件编程 / C 语言 阅读数: 167
收藏 0 赞 0 分享

本文实例为大家分享了使用QPainter绘制3D立方体的具体代码,供大家参考,具体内容如下

1.实现思路

(网上有另一篇类似的,不过他不是用的 Qt 自带的矩阵运算类)

实现思路有点类似使用 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 值,得到每个面的路径,然后绘制表面的路径。

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

2.实现代码

代码 github 链接

实现效果 GIF 动图:

主要代码:

#ifndef MYCUBE_H
#define MYCUBE_H
 
#include <QWidget>
#include <QMouseEvent>
 
#include <QVector3D>
#include <QMatrix4x4>
 
class MyCube : public QWidget
{
  Q_OBJECT
public:
  explicit MyCube(QWidget *parent = nullptr);
 
protected:
  void paintEvent(QPaintEvent *event) override;
  void mousePressEvent(QMouseEvent *event) override;
  void mouseMoveEvent(QMouseEvent *event) override;
  void mouseReleaseEvent(QMouseEvent *event) override;
 
  QPointF getPoint(const QVector3D &vt,int w) const;
 
private:
  QVector<QVector3D> vertexArr;   //八个顶点
  QVector<QVector<int>> elementArr; //六个面
  QMatrix4x4 rotateMat;   //旋转矩阵
  QPoint mousePos;      //鼠标位置
  bool mousePressed=false;  //鼠标按下标志位
};
 
#endif // MYCUBE_H
#include "MyCube.h"
 
#include <QPainter>
#include <QtMath>
#include <QDebug>
 
MyCube::MyCube(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} };
 
  setFocusPolicy(Qt::ClickFocus); //Widget默认没有焦点
}
 
void MyCube::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;
 
  //投影矩阵
  //(奇怪,为什么只是平移了z轴,没用perspective函数就有远小近大的效果,
  //在我的想象中默认不该是正交投影么)
  QMatrix4x4 perspective_mat;
  perspective_mat.translate(0.0f,0.0f,-0.1f);
 
  //计算顶点变换后坐标,包含z值max点就是正交表面可见的,
  //再计算下远小近大的透视投影效果齐活了
  QList<QVector3D> vertex_list; //和矩阵运算后的顶点
  QList<int> vertex_max_list; //top顶点在arr的位置
  float vertex_max_value;   //top值
  //根据旋转矩阵计算每个顶点
  for(int i=0;i<vertexArr.count();i++)
  {
    QVector3D vertex=vertexArr.at(i)*rotateMat*perspective_mat;
    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){
        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)){
        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);
    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<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 MyCube::mousePressEvent(QMouseEvent *event)
{
  mousePressed=true;
  mousePos=event->pos();
  QWidget::mousePressEvent(event);
}
 
void MyCube::mouseMoveEvent(QMouseEvent *event)
{
  if(mousePressed){
    const QPoint posOffset=event->pos()-mousePos;
    mousePos=event->pos();
    //旋转矩阵 x和y分量
    //rotateMat.rotate(posOffset.x(),QVector3D(0.0f,-0.5f,0.0f));
    //rotateMat.rotate(posOffset.y(),QVector3D(0.5f,0.0f,0.0f));
    rotateMat.rotate(1.1f,QVector3D(0.5f*posOffset.y(),-0.5f*posOffset.x(),0.0f));
    update();
  }
  QWidget::mouseMoveEvent(event);
}
 
void MyCube::mouseReleaseEvent(QMouseEvent *event)
{
  mousePressed=false;
  QWidget::mouseReleaseEvent(event);
}
 
QPointF MyCube::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 };
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

更多精彩内容其他人还在看

用标准c++实现string与各种类型之间的转换

这个类在头文件中定义, < sstream>库定义了三种类:istringstream、ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。另外,每个类都有一个对应的宽字符集版本
收藏 0 赞 0 分享

C++如何通过ostringstream实现任意类型转string

再使用整型转string的时候感觉有点棘手,因为itoa不是标准C里面的,而且即便是有itoa,其他类型转string不是很方便。后来去网上找了一下,发现有一个好方法
收藏 0 赞 0 分享

C/C++指针小结

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区
收藏 0 赞 0 分享

C++ 类的静态成员深入解析

在C++中类的静态成员变量和静态成员函数是个容易出错的地方,本文先通过几个例子来总结静态成员变量和成员函数使用规则,再给出一个实例来加深印象
收藏 0 赞 0 分享

C++类的静态成员初始化详细讲解

通常静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域操作符来指出静态成员所属的类.但如果静态成员是整型或是枚举型const,则可以在类声明中初始化
收藏 0 赞 0 分享

C++类静态成员与类静态成员函数详解

静态成员不可在类体内进行赋值,因为它是被所有该类的对象所共享的。你在一个对象里给它赋值,其他对象里的该成员也会发生变化。为了避免混乱,所以不可在类体内进行赋值
收藏 0 赞 0 分享

C++中的friend友元函数详细解析

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。友元函数的特点是能够访问类中的私有成员的非成员函数。友元函数从语法上看,它与普通函数一样,即在定义上和调用上与普通函数一样
收藏 0 赞 0 分享

static全局变量与普通的全局变量的区别详细解析

以下是对static全局变量与普通的全局变量的区别进行了详细的分析介绍,需要的朋友可以过来参考下,希望对大家有所帮助
收藏 0 赞 0 分享

C++ explicit关键字的应用方法详细讲解

C++ explicit关键字用来修饰类的构造函数,表明该构造函数是显式的,既然有"显式"那么必然就有"隐式",那么什么是显示而什么又是隐式的呢?下面就让我们一起来看看这方面的知识吧
收藏 0 赞 0 分享

教你5分钟轻松搞定内存字节对齐

随便google一下,人家就可以跟你解释的,一大堆的道理,我们没怎么多时间,讨论为何要对齐.直入主题,怎么判断内存对齐规则,sizeof的结果怎么来的,请牢记以下3条原则
收藏 0 赞 0 分享
查看更多