最近无聊,用QPainter画了个简单的风车,效果如下:
可以看到很多地方都穿摸了,因为绘制时每个填充路径的顺序没法很好的确定,特别是如果出现两个面交叉更没法处理,我能想到的就是拆分成多个小的三角来计算,不过这样CPU的负担就太大了。
还有一个没解决的是万向锁的问题,我还没搞明白,后面学懂了再回来改下。
整体思路就是先定义对象树结构体,一个绘制对象可以有多个面和子节点。绘制的时候先根据当前角度和位置计算出所有节点的位置和角度,然后通过矩阵运算得到最终的坐标值。最后,根据所有面的z值进行排序,从最远的开始填充。手稿:
代码的github地址(MySimple3D类):https://github.com/gongjianbo/EasyQPainter
主要代码:
#ifndef MYSIMPLE3D_H
#define MYSIMPLE3D_H
#include
#include
#include
#include
#include
#include
//
#include
#include
#include
#include
//图元结构体
struct My3DMeta
{
//顶点,可以是任意个
QList vertex;
//颜色
QColor color;
//QBrush brush;
//图元顶点中z值最小者,单独作为成员便于排序
double z;
//根据定点计算出的路径,便于绘制
QPainterPath path;
My3DMeta(const QList &vtx,const QColor &clr)
:vertex(vtx),color(clr)
{}
};
//物体实体结构体
struct My3DItem
{
//相对于场景或者父节点的坐标位置
QVector3D position;
//相对于场景或者父节点的方向
QVector3D rotation;
//包含的图元
QList> surfaceMetas;
//子节点物体列表
QList> subItems;
//旋转动画因子(根据全局的定时器步进值计算对应分量动画效果)
QVector3D animationFactor;
My3DItem(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(pos),rotation(rotate),surfaceMetas(metas),subItems(subs),animationFactor(factor)
{}
//根据当前位置和角度计算出顶点列表
//position取出后直接叠加到顶点得坐标上:vertex+position+this->position
//rotation目前只计算了x和y的旋转,作用于item的顶点上,毕竟写一个camera太复杂了,所以只能旋转item
//step为定时器动画的步进,每个item根据自身的动画因子成员来计算
QList> calculateSurfaceMetas(const QVector3D &position,const QVector3D &rotation,double step)
{
QVector3D cur_position=position+this->position;
QVector3D cur_rotation=rotation+this->rotation;
for(QSharedPointer meta:surfaceMetas)
{
QPainterPath path;
double z;
//【注意】我这里还没有去解决万向锁的问题,只有叶子节点才能进行z轴旋转
//等把Qt这个QQuaternion类搞明白了再回来改
QMatrix4x4 mat_anim;
mat_anim.rotate(QQuaternion::fromEulerAngles(step*this->animationFactor));
//先在正交投影里计算item旋转后的坐标,然后z+=1000,使z值不至于小于0(因为需要透视投影)
//然后使用透视投影算出近大远小,放大50倍是因为整体太远了看不见
QMatrix4x4 mat_rotate;
mat_rotate.rotate(QQuaternion::fromEulerAngles(cur_rotation));
QMatrix4x4 mat_per;
mat_per.perspective(45.0f,1.0f,0.1f,2000.0f);
mat_per.scale(50); //暂时固定50倍放大
bool is_first=true;
for(const QVector3D &vertex:meta->vertex)
{
QVector3D calc_rotate=(vertex*mat_anim+cur_position)*mat_rotate;
//暂时固定z+=1000
QVector3D calc_vertex=(calc_rotate+QVector3D(0,0,1000))*mat_per;
//qDebug()<path=path;
meta->z=z;
}
QList> surface_metas=surfaceMetas;
for(QSharedPointer item:subItems)
{
surface_metas+=item->calculateSurfaceMetas(cur_position,cur_rotation+step*this->animationFactor,step);
}
return surface_metas;
}
};
//绘制一个3d风车
//目前还存在两个问题:
//1.堆叠顺序计算效果不大好,或许可以将表面裁减为多个小三角来解决;
//2.Qt的四元数类我还不会用,所以物体的相对旋转如果要计算z轴分量就有问题,目前只能叶子节点z轴旋转
class MySimple3D : public QWidget
{
Q_OBJECT
public:
explicit MySimple3D(QWidget *parent = nullptr);
~MySimple3D();
protected:
void paintEvent(QPaintEvent *event) override;
//鼠标操作
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
//改变窗口大小
void resizeEvent(QResizeEvent *event) override;
private:
//初始化操作
void initItems();
//绘制
void drawImage(int width,int height);
private:
//根实体(这个变量绘制时在线程访问)
My3DItem rootItem;
//FPS统计,paintEvent累计temp,达到一秒后赋值给counter
int fpsCounter=0;
int fpsTemp=0;
//FPS计时
QTime fpsTime;
//鼠标位置
QPoint mousePos;
//鼠标按下标志位
bool mousePressed=false;
//旋转角度
double xRotate=0;
double yRotate=0;
//多线程异步watcher
QFutureWatcher watcher;
//绘制好的image
QImage image;
//定时器旋转步进值
double animationStep=0;
};
#endif // MYSIMPLE3D_H
#include "MySimple3D.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
MySimple3D::MySimple3D(QWidget *parent)
: QWidget(parent)
{
initItems();
//异步处理结束,获取结果并刷新窗口
connect(&watcher,&QFutureWatcher::finished,[this](){
image=watcher.result();
update();
});
fpsTime=QTime::currentTime();
fpsTime.start();
//定时旋转风车
QTimer *timer=new QTimer(this);
connect(timer,&QTimer::timeout,[=]{
animationStep+=2.0;
drawImage(width(),height());
});
timer->start(50);
}
MySimple3D::~MySimple3D()
{
if(!watcher.isFinished())
watcher.waitForFinished();
}
void MySimple3D::paintEvent(QPaintEvent *event)
{
event->accept();
QPainter painter(this);
painter.fillRect(this->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 MySimple3D::mousePressEvent(QMouseEvent *event)
{
mousePressed=true;
mousePos=event->pos();
QWidget::mousePressEvent(event);
}
void MySimple3D::mouseMoveEvent(QMouseEvent *event)
{
if(mousePressed){
const QPoint posOffset=event->pos()-mousePos;
mousePos=event->pos();
//旋转矩阵 x和y分量
xRotate+=-posOffset.y();
yRotate+=-posOffset.x();
//update();
drawImage(width(),height());
}
QWidget::mouseMoveEvent(event);
}
void MySimple3D::mouseReleaseEvent(QMouseEvent *event)
{
mousePressed=false;
QWidget::mouseReleaseEvent(event);
}
void MySimple3D::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 MySimple3D::initItems()
{
//模板的嵌套时自动格式化太难看了
//四个扇叶
My3DMeta* sub_fan1=new My3DMeta{{
QVector3D(0,0,0),QVector3D(-250,250,0),QVector3D(-300,200,10),QVector3D(-100,0,10)},
QColor(110,250,250,200)};
My3DMeta* sub_fan2=new My3DMeta{{
QVector3D(0,0,0),QVector3D(-250,-250,0),QVector3D(-200,-300,10),QVector3D(0,-100,10)},
QColor(130,250,250,200)};
My3DMeta* sub_fan3=new My3DMeta{{
QVector3D(0,0,0),QVector3D(250,-250,0),QVector3D(300,-200,10),QVector3D(100,0,10)},
QColor(110,250,250,200)};
My3DMeta* sub_fan4=new My3DMeta{{
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>{};
My3DItem *sub_fanitem=new My3DItem{
QVector3D(0,400,-150),
QVector3D(0,0,0),
sub_fanmetas,
sub_fansubs,
QVector3D(0,0,-1)}; //给z加了动画因子
//风车主干,共9个面,顶部尖塔4+主干4+底面
My3DMeta* sub_main1=new My3DMeta{{
QVector3D(100,400,100),QVector3D(-100,400,100),QVector3D(0,500,0)},
QColor(250,0,0)};
My3DMeta* sub_main2=new My3DMeta{{
QVector3D(-100,400,100),QVector3D(-100,400,-100),QVector3D(0,500,0)},
QColor(0,250,0)};
My3DMeta* sub_main3=new My3DMeta{{
QVector3D(-100,400,-100),QVector3D(100,400,-100),QVector3D(0,500,0)},
QColor(0,0,250)};
My3DMeta* sub_main4=new My3DMeta{{
QVector3D(100,400,-100),QVector3D(100,400,100),QVector3D(0,500,0)},
QColor(250,250,0)};
My3DMeta* sub_main5=new My3DMeta{{
QVector3D(100,400,100),QVector3D(-100,400,100),QVector3D(-120,0,120),QVector3D(120,0,120)},
QColor(205,150,100)};
My3DMeta* sub_main6=new My3DMeta{{
QVector3D(-100,400,100),QVector3D(-100,400,-100),QVector3D(-120,0,-120),QVector3D(-120,0,120)},
QColor(220,150,100)};
My3DMeta* sub_main7=new My3DMeta{{
QVector3D(-100,400,-100),QVector3D(100,400,-100),QVector3D(120,0,-120),QVector3D(-120,0,-120)},
QColor(235,150,100)};
My3DMeta* sub_main8=new My3DMeta{{
QVector3D(100,400,-100),QVector3D(100,400,100),QVector3D(120,0,120),QVector3D(120,0,-120)},
QColor(250,150,100)};
My3DMeta* sub_main9=new My3DMeta{{
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)};
My3DItem *sub_mainitem=new My3DItem{
QVector3D(0,0,0),
QVector3D(0,0,0),
sub_mainmetas,
sub_mainsubs};
//根节点,一个平面,(平面用半透明是为了穿模时看起来没那么别扭)
My3DMeta* root_meta=new My3DMeta{{
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=My3DItem{
QVector3D(0,-300,0),
QVector3D(0,0,0),
root_metas,
root_subs,
QVector3D(0,0.1f,0)}; //给y加了动画因子
}
void MySimple3D::drawImage(int width, int height)
{
if(width>10&&height>10&&watcher.isFinished()){
QVector3D rotate=QVector3D(xRotate,yRotate,0);
int step=animationStep;
//多线程绘制到image上,绘制完后返回image并绘制到窗口上
QFuture futures=QtConcurrent::run([this,width,height,rotate,step](){
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);
//计算所有的图元顶点路径
QList> surface_metas=rootItem.calculateSurfaceMetas(QVector3D(0,0,0),rotate,step);
//根据z轴排序
std::sort(surface_metas.begin(),surface_metas.end(),
[](const QSharedPointer &left,const QSharedPointer &right){
return left->zz;
});
//根据z值从远处开始绘制图元路径
for(QSharedPointer meta:surface_metas)
{
painter.fillPath(meta->path,meta->color);
}
//painter.restore();
return img;
});
watcher.setFuture(futures);
}
}