最近无聊,用QPainter画了个简单的风车,效果如下:
可以看到很多地方都穿摸了,因为绘制时每个填充路径的顺序没法很好的确定,特别是如果出现两个面交叉更没法处理,我能想到的就是拆分成多个小的三角来计算,不过这样CPU的负担就太大了。
整体思路就是先定义对象树结构体,一个绘制对象可以有多个面和子节点。绘制的时候先根据当前角度和位置计算出所有节点的位置和角度,然后通过矩阵运算得到最终的坐标值。最后,根据所有面的z值进行排序,从最远的开始填充。手稿:
(2021-11-17)最近将之前的欧拉角旋转改成了四元数,这样交互的编码更简单点。参照荷兰风车,改为逆时针旋转,扇叶往塔身一侧偏。
代码的github地址(MySimple3D类):https://github.com/gongjianbo/EasyQPainter
主要代码:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//图元结构体
struct WindMeta
{
//顶点,可以是任意个
QList vertex;
//颜色
QColor color;
//QBrush brush;
//图元顶点中z值最小者,单独作为成员便于排序
double z;
//根据定点计算出的路径,便于绘制
QPainterPath path;
//构造函数
WindMeta(const QList &vtx, const QColor &clr);
};
//物体实体结构体
struct WindItem
{
//相对于场景或者父节点的坐标位置
QVector3D position;
//相对于场景或者父节点的方向
QVector3D rotation;
//包含的图元
QList surfaceMetas;
//子节点物体列表
QList subItems;
//旋转动画因子(根据全局的定时器步进值计算对应分量动画效果)
QVector3D animationFactor;
//构造函数
WindItem(const QVector3D &pos = QVector3D(0, 0, 0),
const QVector3D &rotate = QVector3D(0, 0, 0),
const QList &metas = QList(),
const QList &subs = QList(),
const QVector3D &factor = QVector3D(0, 0, 0));
//根据当前位置和角度计算出顶点列表
//position取出后直接叠加到顶点的坐标上:vertex+position+this->position
//rotation目前只计算了x和y的旋转,作用于item的顶点上,目前只能旋转item
//step为定时器动画的步进,每个item根据自身的动画因子成员来计算
QList calcSurfaceMetas(
const QVector3D &position, const QQuaternion &rotation, float step, float fovy);
};
//绘制一个3D风车
//(目前按大块平面来计算堆叠顺序效果不太好,两个物体交叉时会有一部分被覆盖)
class Windmill3D : public QWidget
{
Q_OBJECT
public:
explicit Windmill3D(QWidget *parent = nullptr);
~Windmill3D();
protected:
//显示时才启动定时动画
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
//绘制
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;
//改变窗口大小
void resizeEvent(QResizeEvent *event) override;
private:
//初始化操作
void initWindmill();
//绘制
void drawImage(int width, int height);
private:
//根实体(这个变量绘制时在线程访问)
WindItem rootItem;
//FPS统计,paintEvent累计temp,达到一秒后赋值给counter
int fpsCounter{0};
int fpsTemp{0};
//FPS计时
QTime fpsTime;
//鼠标位置
QPoint mousePos;
//鼠标按下标志位
bool mousePressed{false};
//定时动画
QTimer timer;
//定时器旋转步进值
float animationStep{0.0f};
//观察矩阵旋转
QVector3D rotationAxis;
QQuaternion rotationQuat;
//透视投影的fovy参数,视野范围
float projectionFovy{30.0f};
//多线程异步watcher
QFutureWatcher watcher;
//绘制好的image
QImage image;
};
#include "Windmill3D.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
WindMeta::WindMeta(const QList &vtx, const QColor &clr)
: vertex(vtx), color(clr)
{
}
WindItem::WindItem(const QVector3D &pos, const QVector3D &rotate,
const QList &metas,
const QList &subs,
const QVector3D &factor)
: position(pos), rotation(rotate), surfaceMetas(metas), subItems(subs), animationFactor(factor)
{
}
QList WindItem::calcSurfaceMetas(
const QVector3D &position, const QQuaternion &rotation, float step, float fovy)
{
QVector3D cur_position = position + this->position;
//这里没验证,因为目前只为0,可能有误
QQuaternion cur_rotation = QQuaternion::fromEulerAngles(this->rotation) * rotation;
//平移做裁剪,缩放拉近距离
QMatrix4x4 perspective_mat;
perspective_mat.scale(100);
perspective_mat.perspective(fovy, 1.0f, 0.1f, 2000.0f);
QMatrix4x4 view_mat;
view_mat.translate(0.0f, 0.0f, -1000.0f);
//先跟随父节点转动和位移,再以自身的转动步进进行转动
QMatrix4x4 model_mat;
model_mat.rotate(cur_rotation);
model_mat.translate(cur_position);
model_mat.rotate(QQuaternion::fromEulerAngles(step * this->animationFactor));
for (QSharedPointer meta : surfaceMetas)
{
QPainterPath path;
double z;
bool is_first = true;
for (const QVector3D &vertex : meta->vertex)
{
QVector3D calc_vertex= perspective_mat * view_mat * model_mat * vertex;
calc_vertex.setY(-calc_vertex.y());
calc_vertex.setZ(-calc_vertex.z());
//qDebug()rect(), Qt::black);
if (image.size().isValid())
painter.drawImage(0, 0, image);
//fps统计
if (fpsTime.elapsed() > 1000)
{
fpsTime.restart();
fpsCounter = fpsTemp;
fpsTemp = 0;
}
else
{
fpsTemp++;
}
painter.setPen(QPen(Qt::white));
painter.drawText(10, 30, "FPS:" + QString::number(fpsCounter));
painter.drawText(10, 50, "Drag Moving ... ...");
}
void Windmill3D::mousePressEvent(QMouseEvent *event)
{
mousePressed = true;
mousePos = event->pos();
QWidget::mousePressEvent(event);
}
void Windmill3D::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();
drawImage(width(), height());
}
QWidget::mouseMoveEvent(event);
}
void Windmill3D::mouseReleaseEvent(QMouseEvent *event)
{
mousePressed = false;
QWidget::mouseReleaseEvent(event);
}
void Windmill3D::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();
drawImage(width(), height());
}
void Windmill3D::resizeEvent(QResizeEvent *event)
{
if (event->size().isValid())
{
const int width = event->size().width();
const int height = event->size().height();
drawImage(width, height);
}
QWidget::resizeEvent(event);
}
void Windmill3D::initWindmill()
{
//参照荷兰风车,逆时针旋转,帆布往塔身一侧倾斜
//四个扇叶
WindMeta *sub_fan1 = new WindMeta{{QVector3D(0, 0, 0), QVector3D(-250, -250, 0),
QVector3D(-300, -200, -10), QVector3D(-100, 0, -10)},
QColor(110, 250, 250, 200)};
WindMeta *sub_fan2 = new WindMeta{{QVector3D(0, 0, 0), QVector3D(-250, 250, 0),
QVector3D(-200, 300, -10), QVector3D(0, 100, -10)},
QColor(130, 250, 250, 200)};
WindMeta *sub_fan3 = new WindMeta{{QVector3D(0, 0, 0), QVector3D(250, 250, 0),
QVector3D(300, 200, -10), QVector3D(100, 0, -10)},
QColor(110, 250, 250, 200)};
WindMeta *sub_fan4 = new WindMeta{{QVector3D(0, 0, 0), QVector3D(250, -250, 0),
QVector3D(200, -300, -10), QVector3D(0, -100, -10)},
QColor(130, 250, 250, 200)};
auto sub_fanmetas = QList{QSharedPointer(sub_fan1),
QSharedPointer(sub_fan2),
QSharedPointer(sub_fan3),
QSharedPointer(sub_fan4)};
auto sub_fansubs = QList{};
WindItem *sub_fanitem = new WindItem{
QVector3D(0, 400, 150), //相对位置,y400放到顶部,z150贴在墙上
QVector3D(0, 0, 0), //相对方向
sub_fanmetas,
sub_fansubs,
QVector3D(0, 0, 1)}; //给z加了动画因子,即扇叶在xy平面转
//风车主干,共9个面,顶部尖塔4+主干4+底面
//顶部4
WindMeta *sub_main1 = new WindMeta{{QVector3D(100, 400, 100), QVector3D(-100, 400, 100), QVector3D(0, 500, 0)},
QColor(250, 0, 0)};
WindMeta *sub_main2 = new WindMeta{{QVector3D(-100, 400, 100), QVector3D(-100, 400, -100), QVector3D(0, 500, 0)},
QColor(0, 250, 0)};
WindMeta *sub_main3 = new WindMeta{{QVector3D(-100, 400, -100), QVector3D(100, 400, -100), QVector3D(0, 500, 0)},
QColor(0, 0, 250)};
WindMeta *sub_main4 = new WindMeta{{QVector3D(100, 400, -100), QVector3D(100, 400, 100), QVector3D(0, 500, 0)},
QColor(250, 250, 0)};
//主体4
WindMeta *sub_main5 = new WindMeta{{QVector3D(100, 400, 100), QVector3D(-100, 400, 100),
QVector3D(-120, 0, 120), QVector3D(120, 0, 120)},
QColor(205, 150, 100)};
WindMeta *sub_main6 = new WindMeta{{QVector3D(-100, 400, 100), QVector3D(-100, 400, -100),
QVector3D(-120, 0, -120), QVector3D(-120, 0, 120)},
QColor(220, 150, 100)};
WindMeta *sub_main7 = new WindMeta{{QVector3D(-100, 400, -100), QVector3D(100, 400, -100),
QVector3D(120, 0, -120), QVector3D(-120, 0, -120)},
QColor(235, 150, 100)};
WindMeta *sub_main8 = new WindMeta{{QVector3D(100, 400, -100), QVector3D(100, 400, 100),
QVector3D(120, 0, 120), QVector3D(120, 0, -120)},
QColor(250, 150, 100)};
//底部1
WindMeta *sub_main9 = new WindMeta{{QVector3D(-120, 0, 120), QVector3D(-120, 0, -120),
QVector3D(120, 0, -120), QVector3D(120, 0, 120)},
QColor(200, 150, 0)};
auto sub_mainmetas = QList{QSharedPointer(sub_main1),
QSharedPointer(sub_main2),
QSharedPointer(sub_main3),
QSharedPointer(sub_main4),
QSharedPointer(sub_main5),
QSharedPointer(sub_main6),
QSharedPointer(sub_main7),
QSharedPointer(sub_main8),
QSharedPointer(sub_main9)};
auto sub_mainsubs = QList{QSharedPointer(sub_fanitem)};
WindItem *sub_mainitem = new WindItem{
QVector3D(0, 0, 0), //相对位置
QVector3D(0, 0, 0), //相对方向
sub_mainmetas,
sub_mainsubs};
//根节点,一个平面,(平面用半透明是为了穿模时看起来没那么别扭)
WindMeta *root_meta = new WindMeta{{QVector3D(-200, 0, 200), QVector3D(200, 0, 200),
QVector3D(200, 0, -200), QVector3D(-200, 0, -200)},
QColor(255, 255, 255, 100)};
auto root_metas = QList{QSharedPointer(root_meta)};
auto root_subs = QList{QSharedPointer(sub_mainitem)};
rootItem = WindItem{
QVector3D(0, -300, 0), //相对位置,y轴-300相当于放到了底部
QVector3D(0, 0, 0), //相对方向
root_metas,
root_subs,
QVector3D(0, -0.1f, 0)}; //给y加了动画因子,即柱子在xz平面转
}
void Windmill3D::drawImage(int width, int height)
{
if (width > 10 && height > 10 && watcher.isFinished())
{
QQuaternion rotate = rotationQuat;
float step = animationStep;
float fovy = projectionFovy;
//多线程绘制到image上,绘制完后返回image并绘制到窗口上
QFuture futures = QtConcurrent::run([this, width, height, rotate, step, fovy]()
{
QImage img(width, height, QImage::Format_ARGB32);
img.fill(Qt::transparent);
QPainter painter(&img);
if (!painter.isActive())
return img;
painter.fillRect(img.rect(), Qt::black);
//painter.save();
//坐标原点移动到中心
painter.translate(width / 2, height / 2);
//抗锯齿
painter.setRenderHint(QPainter::Antialiasing);
//计算所有的图元顶点路径
QList surface_metas = rootItem.calcSurfaceMetas(
QVector3D(0, 0, 0), rotate, step, fovy);
//根据z轴排序
std::sort(surface_metas.begin(), surface_metas.end(),
[](const QSharedPointer &left, const QSharedPointer &right)
{
return left->z < right->z;
});
//根据z值从远处开始绘制图元路径
for (QSharedPointer meta : surface_metas)
{
painter.fillPath(meta->path, meta->color);
}
//painter.restore();
return img;
});
watcher.setFuture(futures);
}
}