QT使用OpenGL实现水波特效 您所在的位置:网站首页 做水的特效软件 QT使用OpenGL实现水波特效

QT使用OpenGL实现水波特效

2023-09-12 12:44| 来源: 网络整理| 查看: 265

前言

近期花了一些时间自学了openGL的基础知识,因为在网页中看到了一个不错的水波特效,并且在github上找到了使用javascript和webGL实现的代码,就想在QT中实现一个类似的,不知道是不是我的搜索方法不对,找到了一些qt水波特效的实现,但抛开应用容易与否不谈,总觉得特效本身的效果不如js中的这个效果好,而且也没找到使用这种方法实现特效的QT程序(有一个使用该方法但没使用openGL的)。

经过一段时间的改进,这个动态水波特效背景已经可以在一些场景下简便的使用了,本文会介绍代码的具体用法。

本文会给出demo的github地址,并简单讲解实现特效的原理和方式。

效果图

在这里插入图片描述

环境

windows7 QT Creator 4.5.1 QT 5.10.1 MSVC2017_x64 需要支持openGL,现代操作系统一般应该都是支持的,但是有可能存在版本问题。

完整代码

项目代码已上传至github

主要参考的js项目代码jquery.ripples

另一个更为精彩但可能不是那么方便应用的demo webgl-water

使用说明

从github下载代码,并且将ripplewidget.h和ripplewidget.cpp添加到你的项目工程中。

举例来说,假设你想在名为Dialog的继承自QDialog类的对话框中添加一个水波背景,你可以如下编写代码:

#ifndef DIALOG_H #define DIALOG_H #include #include "ripplewidget.h" namespace Ui { class Dialog; } class Dialog : public QDialog { Q_OBJECT public: explicit Dialog(QWidget *parent = 0); ~Dialog(); private: Ui::Dialog *ui; RippleWidget* m_rippleWidget; }; #endif // DIALOG_H #include "dialog.h" #include "ui_dialog.h" Dialog::Dialog(QWidget *parent) : QDialog(parent), ui(new Ui::Dialog),m_rippleWidget(nullptr) { ui->setupUi(this); m_rippleWidget=new RippleWidget(this);//create ripplewidget as dialog's child m_rippleWidget->lower();//put ripplewidget on the bottom //parametes can be changed m_ripplewidget->setRadius(20); m_ripplewidget->setStrength(0.01); m_ripplewidget->setResolution(2.0); m_ripplewidget->setDamping(0.995); m_rippleWidget->setBackgroundImage(":/img/bg2.jpg"); } Dialog::~Dialog() { if(m_rippleWidget) { delete m_rippleWidget; } delete ui; } 水波参数设置及说明

半径 - setRadius(int radius) 默认值为20。注意实际的水波大小还受分辨率参数的影响。

强度 - setStrength(float strenght) 默认值为0.01。用来设置水波强度,可以理解成振幅。

分辨率 - setResolution(float resolution) 默认值为2.0。注意此参数并不改变背景图片的分辨率,影响的是水波的精细程度,并且会影响水波的传播速度和半径。目前这个参数可能有点迷惑性,因为参数数值越大,实际分辨率越低,水波精细程度越低。

衰减 - setDamping(float damping) 默认值为0.995。影响水波的衰减速率,有效范围为0~1(开区间),数值越大衰减越慢。

背景图片 - setBackgroundImage(string filepath) 虽然我也设置了默认值,不过这个默认值没什么意义,我并没有将默认图片保存在代码中。如果没能正确设置背景图片,ripplewidget将会呈现黑色,不过此时应该仍然能隐约看到水波特效。

各参数共同影响水波的实际效果,而且并非水波越精细效果越好,你可能需要调整若干参数来获得令人满意的最终效果。

使用函数添加水波

void RippleWidget::drop(int x,int y,int radius,float strength);函数可以用来向RippleWidget添加新的波源,参数依次为:横坐标,纵坐标,半径,强度。 实际上跟随鼠标产生水波也是通过重写MouseEvent,在其中调用此函数生成的。

其他注意事项

考虑到使用此特效时很可能是将其作为背景层叠在其他控件之下,想要RippleWidget能有正确的表现有两个需要注意的问题:

一是如何让RippleWidget在父窗口大小改变时能够正确的调整自身的大小。

二是如何让RippleWidget正确获得鼠标事件。一般来讲如果上层控件需要处理鼠标事件,那么此时RippleWidget接收不到鼠标事件是可以接受的,对于鼠标点击事件来说很可能并不需要额外的改动就能获得预想的行为,但是对于鼠标移动事件就可能有问题,由于QT的MouseTracking默认是false,如果你发现只有按住鼠标时RippleWidget才能响应鼠标移动,那就很有可能是你没有正确设置某个上层控件的MouseTracking造成的。

目前为了使设置比较简便,用的是在RippleWidget中设置事件过滤器的方式,构造函数RippleWidget::RippleWidget(QWidget* parent=0,bool insfilter=true);默认会设置事件过滤器监听父窗口的事件并作出响应(可以令instilter=false来取消监听),但是依然存在父窗口本身无法获得鼠标事件甚至更复杂的情况,可能导致RippleWidget无法正确工作。

我现在并不知道更好的办法应对比较复杂的界面。在我的demo中,为了能让RippleWidget在MainWindow内正常表现,我通过在MainWindow内设置事件过滤器达到了预期,也试过重写事件函数的方式,也达到了预想的效果。 但是总之,当界面比较复杂的时候,很可能无法通过简单的设置取得满意的效果,必要的时候需要重写事件函数或者改写我的代码,此时就需要各位自行发挥了。

特效基本原理

让我们先把问题下降一维,垂直背景图片的平面切一刀,取截面图方便理解。 在这里插入图片描述 在这里插入图片描述 现在我们有一张带有两道不同颜色宽直线的背景图片。假设在某一时刻水面的横截面图如上图所示是折线型,那么我们透过此时的水面垂直向背景图片看去,就应该能看到上图下方带三道宽直线的图形。 上图仅是示意,未必正确的表示了光的折射,但是,至少我们应该知道,当我们拥有足够多的信息时,我们应该有一个从视线坐标x到背景图片坐标x’的映射。 那么,如果我们在在坐标x处取背景图x’处的颜色并渲染到最终的图片,我们就能得到一张经过水面折射的背景图了。

然后再回到三维空间,虽然可能无法轻而易举的推广,但我们姑且从结论出发,视线坐标(x,y)也会有一个到背景图片(x’,y’)的映射。那么如法炮制,在坐标(x,y)处取(x’,y’)的颜色并渲染,就可以得到经过折射后的背景图片。

基本思路即是如此,要实现最终目标,我们就得知道我们需要哪些数据,如何通过这些数据计算出(x’,y’)。

实现方法的部分具体细节

实际上要想计算出(x’,y’),我们需要(x,y)处的水面法线向量和水面到背景图片平面的距离。而如果我们拥有此时整个xOy平面内的水面高度数据,就可以计算出这两者。

在查找资料的过程中,看到有人似乎用sin函数来模拟水面高度,按照这个思路成功绘制了水波,但是没有具体去查看代码。

既然sin函数可以模拟水波截面,那么可以有一个相对直观的想法,我们记录每个水波的波源中心,通过坐标与波源中心的位置和时间参数来计算各坐标处的水面高度。 然而尽管这种想法相对符合直觉,实现起来也是颇为麻烦,而且这种做法使用了相对耗时的sin函数,还需要记录每一个波源的位置直至消失,可以认为这种做法是以水波为对象,有一个水波就得创建一个实例,当波源较多时,可以想象性能和效果会受到较大的影响。

使用OpenGL进行绘制的一个优势是可以将一部分运算交给GPU,而配合我所参考的代码中的一个神奇的算法,计算整个水面各点高度的过程会变得非常迅速,而且无论此时有多少波源,也由此可以绘制相当顺滑的水波特效了。

OpenGL相关代码的详细解释

关于着色器中使用的算法原理及其解释推导,最终我查阅了资料并做了我认为比较详细的解释。可以移步使用OpenGL模拟水面查看具体的分析。 下方的内容是之前学习和查阅的资料,与此项目使用的方式并不完全吻合,姑且先留着吧。

一个神奇的算法

这个算法用巧妙的方式把水面看做一个整体,每次计算更新,都能够获得整个平面当前水面的高度,而与此时有多少个波源无关。

一个尚可考的页面中对此算法作出了一定的解释,但是这里他也是不知道该算法的发明者是谁,而且解释有些简略难懂。 2D Water

简单讲,就是我们需要两个连续的帧,每个帧中的数据即是水面高度。通过这个算法,可以由两个连续的帧计算出下一帧的数据。假设此连续的两帧为T1,T2,那么我们可以算出下一帧T3,之后舍弃T1,由T2,T3两帧我们又能进一步计算出下一帧……如此往复,我们能够获得接下来所有的帧,就能模拟出水波在整个平面内传播的效果。 水波的扩散传播甚至碰壁反弹的效果其实都是在这部分算法中完成的。

你或许可以在动态水面模拟一文中找到该算法的详细解释和推导。

代码的基本组成

这里不详细展开,仅做简要说明,我并没有能把上述的原理的所有细节与实际代码一一对应起来,只能说大概知道各部分的对应关系。想更深入研究可以直接下载阅读代码。 上述算法主要是在openGL的ShaderProgram中实现的,如果你对openGL完全不了解,那想搞清楚该算法是如何工作的可能需要花相当的时间去学习。

此程序中一共使用了3段openGL程序:

其中drop_program负责添加新的波源。

update_program即是精髓部分,使用两个帧缓冲存储两帧的数据,配合在QT程序中updateFrame函数,完成了前段所述的操作,计算出当前帧整个平面内的水面高度。 更具体点说,帧缓冲将数据储存为纹理,实际上两个帧缓冲储存的并非是分别存储T1,T2,每个帧缓冲都通过r分量储存T1,g分量储存T2,以便render_program可以通过帧缓冲获得两帧的水面高度数据。

render_program负责取得帧缓冲中的水面高度数据,并以此计算出水面法向量和经过折射的偏移量,最终获得上述的(x’,y’);再由背景图像纹理及刚计算出的坐标(x’,y’),渲染最终的图像至屏幕。

最后

至少我觉得自己没有食言,花了一段时间把这个项目完成到了我期望的程度。 虽然这项目依旧谈不上成熟,虽然类似的特效已经在网页中玩出花来了,虽然核心的算法不是我自己发明的,但怎么说呢,实现这个demo还是挺开心的,这里就当做是给使用qt的同学们抛砖引玉一下吧。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有