您当前的位置: 首页 >  3d

龚建波

暂无认证

  • 4浏览

    0关注

    313博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

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

龚建波 发布时间:2020-05-16 21:43:19 ,浏览量:4

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};
}
关注
打赏
1655829268
查看更多评论
立即登录/注册

微信扫码登录

0.1540s