首页 > 代码库 > Qt5官方demo解析集12——Qt Quick Particles Examples - CustomParticles

Qt5官方demo解析集12——Qt Quick Particles Examples - CustomParticles

本系列所有文章可以在这里查看http://blog.csdn.net/cloud_castle/article/category/2123873

接上文Qt5官方demo解析集11——Qt Quick Particles Examples - Affectors

使用Emitter和Affectors强大的功能,我们已经可以构造出丰富多彩的粒子特效,但当这些功能还不能满足我们的需要时,我们可以转而采用CustomParticle取代ImageParticle,在CustomParticle中我们可以使用基于GLSL的渲染技术来创建自定义的粒子。

这个demo依然由一些小例子组成,不过比前两个demo中的都要少,只有三个:



(1)Blur Particles

在这个例子中我们可以看到使用CustomParticle创建模糊化粒子的方法。运行效果如下:



个人感觉在这个演示中模糊效果并不是很清楚,于是换了一张图片,并类似地使用不带模糊化的ImageParticle来展示它们的不同:


可以看到,左边图为模糊化的CustomParticle,右边为没有模糊效果的ImageParticle,区别还是很明显的。

由于笔者对OpenGL并不是很熟悉,如果有错误的地方,还请各位指正。

需要指明的是,对于这种内嵌的GLSL代码,Qt为我们提供了一些提前定义好的变量。用uniform和attribute类型定义,作为vertexShader的输入。比如uniform mat4 qt_Matrix —— 提供了一个从根项目到ShaderEffect的变换矩阵;uniform float qt_Opacity —— 提供项目的透明度;attribute vec4 qt_Vertex ——提供顶点坐标,左上角为(0,0),后下角为(width,height);attribute vec2 qt_MultiTexCoord0 —— 纹理坐标,左上角是(0,0),右下角为(1,1)。


另外,QML中任何类型的属性都可以映射到GLSL的uniform类型中去。也就是说,当我们在QML中定义了一个4维RGBA的QColor color,然后再GLSL中声明一个uniform vec4 color,就可以直接使用它了。我们也可以将类型声明为var,Qt会自动帮我们完成转换。下面的例子中大量使用了这种方法。详细的类型映射可以参考Manual中的ShaderEffect

看源码时,我们先看一段额外的代码,我们将其命名为vertexShader_addin:

attribute highp vec2 qt_ParticlePos;   // 这些是预定义好的属性
attribute highp vec2 qt_ParticleTex;
attribute highp vec4 qt_ParticleData; //  x = time,  y = lifeSpan, z = size,  w = endSize
attribute highp vec4 qt_ParticleVec; // x,y = constant velocity,  z,w = acceleration
attribute highp float qt_ParticleR;
uniform highp mat4 qt_Matrix;
uniform highp float qt_Timestamp;
varying highp vec2 qt_TexCoord0;       // 默认的纹理坐标
void defaultMain() {
    qt_TexCoord0 = qt_ParticleTex;
    highp float size = qt_ParticleData.z;
    highp float endSize = qt_ParticleData.w;
    highp float t = (qt_Timestamp - qt_ParticleData.x) / qt_ParticleData.y;
    highp float currentSize = mix(size, endSize, t * t);
    if (t < 0. || t > 1.)
        currentSize = 0.;
    highp vec2 pos = qt_ParticlePos
                   - currentSize / 2. + currentSize * qt_ParticleTex   // adjust size
                   + qt_ParticleVec.xy * t * qt_ParticleData.y         // apply velocity vector..
                   + 0.5 * qt_ParticleVec.zw * pow(t * qt_ParticleData.y, 2.);
    gl_Position = qt_Matrix * vec4(pos.x, pos.y, 0, 1);
}
至于它是什么,我们后面再说。

整个例子的源码如下,blurparticle.qml:

import QtQuick 2.0
import QtQuick.Particles 2.0

Rectangle {
    color: "white"
    width: 240
    height: 360
    ParticleSystem {
        id: sys
    }
    Emitter {                     // Emitter就不再多做介绍了,不熟悉的朋友可以查看前两篇博文
        system:sys
        height: parent.height
        emitRate: 1
        lifeSpan: 12000
        velocity: PointDirection {x:20;}
        size: 128
    }
    ShaderEffectSource {                     // 着色效果源,用来指明需要着色的对象
        id: theSource
        sourceItem: theItem                  // 指明源对象
        hideSource: true                     // 隐藏原图
    }
    Image {
        id: theItem
        source: "../../images/starfish_1.png"
    }

    CustomParticle {                        // CustomParticle内部仅有两个属性,分别是vertexShader和fragmentShader(顶点着色器与片元着色器),如果你熟悉OpenGL以及GLSL应该对这两个东西不陌生。这两个属性参数为"string",实际也就是GLSL的代码。我们可以使用这两个属性来将CustomParticle渲染成各种自定义的效果
        system: sys 
        //! [vertex]
        vertexShader:"                       // 这里是我们的第一个顶点着色器,当我们编写完vertexShader代码后,Qt会为我们将vertexShader_addin中的代码添加到这一行之后。这些代码定义了一些基本的变量,以及一个defaultMain()函数,vertexShader中最基本的gl_Position的输出也由该函数段完成,它为我们实现了基本的粒子功能。
            uniform lowp float qt_Opacity;   // 定义一个只读的低精度浮点型变量qt_Opacity
            varying lowp float fFade;        // 定义一个由vertex写入,fragment读出的低精度浮点型变量fFade
            varying lowp float fBlur;

            void main() {                   // GLSL中规定的程序入口                                         
                defaultMain();              // 该函数在vertexShader_addin中定义,我们需要先调用它
                highp float t = (qt_Timestamp - qt_ParticleData.x) / qt_ParticleData.y;  // 可以看到qt_XX也是在vertexShader_addin中定义的,这里计算了粒子存在时间占生命周期的比例
                highp float fadeIn = min(t * 10., 1.);                       // 比例t 从0变化到1时,fadeIn也从0变化到1
                highp float fadeOut = 1. - max(0., min((t - 0.75) * 4., 1.));  // t 从0.75变化到1时,fadOut从1变化到0

                fFade = fadeIn * fadeOut * qt_Opacity;           // 该值越小,图像透明度越高
                fBlur = max(0.2 * t, t * qt_ParticleR);          // 模糊系数
            }
        "
        //! [vertex]
        property variant source: theSource                       // 这里回到QML代码,定义了类型为variant的属性source以及blurred,为了下面的映射
        property variant blurred: ShaderEffectSource {           // 这里再次使用了ShaderEffectSource,并将返回值赋给我们的自定义属性blurred
        sourceItem: ShaderEffect {                               // sourceItem是其属性成员之一,参数类型为Item,而ShaderEffect继承于Item
            width: theItem.width                                 // 定义为图像的高宽
            height: theItem.height 
            property variant delta: Qt.size(0.0, 1.0 / height)   // 定义变量增量,与高度负相关
            property variant source: ShaderEffectSource {        // 定义属性source指向另一个ShaderEffectSource
                sourceItem: ShaderEffect {
                    width: theItem.width
                    height: theItem.height
                    property variant delta: Qt.size(1.0 / width, 0.0)  // 该增量与宽度相关
                    property variant source: theSource                 
                    fragmentShader: "                                  // 片元着色器
                        uniform sampler2D source;                      // 从图片源采集纹理数据,这里的source在QML代码中定义
                        uniform lowp float qt_Opacity;           // qt_Opacity由Qt预定义,从vertexShader传入
                        uniform highp vec2 delta;
                        varying highp vec2 qt_TexCoord0;
                        void main() {                     // 然后我们在main()函数中定义每个像素点的像素值,下面的算式将每个像素点周围的的值进行叠加来得到新的像素值,从而形成模糊的效果
                            gl_FragColor =(0.0538 * texture2D(source, qt_TexCoord0 - 3.182 * delta)  // 使用第二个参数中的左边对source中的纹理进行采样
                                         + 0.3229 * texture2D(source, qt_TexCoord0 - 1.364 * delta)
                                         + 0.2466 * texture2D(source, qt_TexCoord0)
                                         + 0.3229 * texture2D(source, qt_TexCoord0 + 1.364 * delta)
                                         + 0.0538 * texture2D(source, qt_TexCoord0 + 3.182 * delta)) * qt_Opacity;
                        }"
                }
            }
            fragmentShader: "
                uniform sampler2D source;
                uniform lowp float qt_Opacity;
                uniform highp vec2 delta;
                varying highp vec2 qt_TexCoord0;
                void main() {                   // 第一次混合以宽度作为增量,第二次以高度作为增量。这里的source是已经被处理过一次的纹理源
                    gl_FragColor =(0.0538 * texture2D(source, qt_TexCoord0 - 3.182 * delta)
                                 + 0.3229 * texture2D(source, qt_TexCoord0 - 1.364 * delta)
                                 + 0.2466 * texture2D(source, qt_TexCoord0)
                                 + 0.3229 * texture2D(source, qt_TexCoord0 + 1.364 * delta)
                                 + 0.0538 * texture2D(source, qt_TexCoord0 + 3.182 * delta)) * qt_Opacity;
                }"
            }
        }
        //! [fragment]
        fragmentShader: "                   // 最后将vertexShader中定义的数据送入fragmentShader中进行处理
            uniform sampler2D source;         
            uniform sampler2D blurred;          
            varying highp vec2 qt_TexCoord0;
            varying highp float fBlur;
            varying highp float fFade;
            void main() {
                gl_FragColor = mix(texture2D(source, qt_TexCoord0), texture2D(blurred, qt_TexCoord0), min(1.0,fBlur*3.0)) * fFade;  // 在每个像素点对源纹理纹理与模糊化纹理之间进行插值,并乘以透明度。
            }"
        //! [fragment]

    }
}


(2)Fragment Shader

在上个例子我们对使用CustomParticle渲染一个外部png图像有了一个大致印象,在这个例子中,Qt 向我们介绍了如何直接使用片元着色器来绘制粒子。


屏幕下方的话也揭示了这个例子的主题。


这个例子的层次的结构没有上一个例子那么复杂,来看看吧,fragmentshader.qml:

import QtQuick 2.0
import QtQuick.Particles 2.0

ParticleSystem {                             // ParticleSystem作为根目录
    id: root
    width: 320
    height: 480
    Rectangle {                                   // 矩形背景
        z: -1
        anchors.fill: parent
        color: "black"
        Text {                                      // 文字
            anchors.bottom: parent.bottom
            anchors.horizontalCenter: parent.horizontalCenter
            font.pixelSize: 14
            color: "white"
            text: "It's all in the fragment shader."
        }
    }

    Emitter {
        emitRate: 400
        lifeSpan: 8000
        size: 24
        sizeVariation: 16
        velocity: PointDirection {x: root.width/10; y: root.height/10;}
        acceleration: PointDirection {x: -root.width/40; y: -root.height/40; xVariation: -root.width/20; yVariation: -root.width/20}
    }

    CustomParticle {                        
        vertexShader:"                              // 顶点着色器
            uniform lowp float qt_Opacity;          // 变量定义
            varying lowp float fFade;
            varying highp vec2 fPos;

            void main() {                        // 还记得上面的vertexShader_addin吗,下面的只是将defaltMain()中的代码提出来了
                qt_TexCoord0 = qt_ParticleTex;
                highp float size = qt_ParticleData.z;
                highp float endSize = qt_ParticleData.w;

                highp float t = (qt_Timestamp - qt_ParticleData.x) / qt_ParticleData.y;

                highp float currentSize = mix(size, endSize, t * t);

                if (t < 0. || t > 1.)
                currentSize = 0.;

                highp vec2 pos = qt_ParticlePos
                - currentSize / 2. + currentSize * qt_ParticleTex          // adjust size
                + qt_ParticleVec.xy * t * qt_ParticleData.y         // apply velocity vector..
                + 0.5 * qt_ParticleVec.zw * pow(t * qt_ParticleData.y, 2.);

                gl_Position = qt_Matrix * vec4(pos.x, pos.y, 0, 1);     // 以上都是defaultMain()的代码,之所以不用defaultMain(),是因为该函数中定义的一些变量在下面还要被继续使用

                highp float fadeIn = min(t * 20., 1.);                  // 与上一个例子类似的定义了与t相关的fadeIn与fadeOut
                highp float fadeOut = 1. - max(0., min((t - 0.75) * 4., 1.));

                fFade = fadeIn * fadeOut * qt_Opacity;               // 得到粒子透明度的动态变化数值
                fPos = vec2(pos.x/320., pos.y/480.);                 // 这里得到位置的二维矢量
            }
        "
        //! [0]
        fragmentShader: "                                     // 片元着色器
            varying highp vec2 fPos;                          // 传参
            varying lowp float fFade;
            varying highp vec2 qt_TexCoord0;
            void main() {//*2 because this generates dark colors mostly  // 官方注释
                highp vec2 circlePos = qt_TexCoord0*2.0 - vec2(1.0,1.0);  // qt_TexCoord0是一个内置的渲染坐标,这个算式将坐标原点转移到了矩形中心,并放大了一倍
                highp float dist = length(circlePos);                    // GLSL内置函数,用来求矢量长度
                highp float circleFactor = max(min(1.0 - dist, 1.0), 0.0); // 在一个长度为2矩形框内,以长度为1在中心划一个区域,那就是一个内切圈
                gl_FragColor = vec4(fPos.x*2.0 - fPos.y, fPos.y*2.0 - fPos.x, fPos.x*fPos.y*2.0, 0.0) * circleFactor * fFade; // 最后将每个像素点的像素值乘上这个圆因子,使绘制出来的圆形越靠近中心RGB值越大,远端小的RGB值被绘制为黑色,从而得到了颜色越来越淡的"圆形粒子"。由四维矢量vec4()的4个参数可以知道,x值越大R值越大,y值越大G值越大,x,y同时影响B的值。那么上方的粒子应该偏红色,下方的粒子偏绿色,右下角的粒子为RGB值都大的紫色,湛蓝色,深灰色等。运行的实际效果也如我们猜想的一样。
            }"
        //! [0]

    }
}


(3)Image Color

在这个例子中,Qt向我们展示了一副图像被“粒子化”的过程,与我们之前用shape覆盖图像不同,这里的粒子颜色会随着图片中当前像素的变化而变化。



在屏幕上点击过后,图像会以粒子的形态展现出来,然后向四周发散。imagecolor.qml:

import QtQuick 2.0
import QtQuick.Particles 2.0

Rectangle {                                          // 一个矩形用来作为窗口边界
    width: 400
    height: 400
    Rectangle {                                        // 一个矩形又来限制图像尺寸
        id: root
        color: "white"
        width: 310
        height: 300
        anchors.centerIn: parent
        ParticleSystem { id: sys }
        CustomParticle {
            system: sys
            property real maxWidth: root.width              // 定义了最大宽高
            property real maxHeight: root.height
            ShaderEffectSource {                          // 使用这个类型提供图像的纹理数据
                id: pictureSource
                sourceItem: picture
                hideSource: true
            }
            Image {                                        // 海星星,我们也可以换成其他的图片
                id: picture
                source: "qrc:/images/starfish_3.png"
            }
            ShaderEffectSource {                           // 第二个ShaderEffectSource用来支持粒子
                id: particleSource
                sourceItem: particle
                hideSource: true
            }
            Image {                                        // ImageParticle中常用的fuzzydot,光点
                id: particle
                source: "qrc:///particleresources/fuzzydot.png"
            }
            //! [vertex]
            vertexShader:"
                uniform highp float maxWidth;              // 向GLSL传参
                uniform highp float maxHeight;
                varying highp vec2 fTex2;                  // 该参数用来计算点在当前图像上的位置
                varying lowp float fFade;                  // 这个参数应该不陌生了,提供渐隐效果
                uniform lowp float qt_Opacity;             

                void main() {

                    fTex2 = vec2(qt_ParticlePos.x, qt_ParticlePos.y);
                    //Uncomment this next line for each particle to use full texture, instead of the solid color at the center of the particle.
                    //fTex2 = fTex2 + ((- qt_ParticleData.z / 2. + qt_ParticleData.z) * qt_ParticleTex); //Adjusts size so it's like a chunk of image.
                    fTex2 = fTex2 / vec2(maxWidth, maxHeight);
                    highp float t = (qt_Timestamp - qt_ParticleData.x) / qt_ParticleData.y;
                    fFade = min(t*4., (1.-t*t)*.75) * qt_Opacity;
                    defaultMain();
                }
            "
            //! [vertex]
            property variant particleTexture: particleSource
            property variant pictureTexture: pictureSource
            //! [fragment]
            fragmentShader: "
                uniform sampler2D particleTexture;             // 然后对粒子纹理采样
                uniform sampler2D pictureTexture;              // 对图像纹理采样
                varying highp vec2 qt_TexCoord0;  // 该矢量相当于包含了(0,0)到(1,1)的所有像素点,如果基于它采样,每个粒子都被渲染成这个图像的样子
                varying highp vec2 fTex2;        // 因此引入这个比例矢量,用来仅仅提供一个像素点的坐标
                varying lowp float fFade;
                void main() {
                    gl_FragColor = texture2D(pictureTexture, fTex2) * texture2D(particleTexture, qt_TexCoord0).w * fFade; // 因此第一个因子用来提供颜色,第二个因子用来提供形状,最后一个因子提供渐隐的效果
            }"
            //! [fragment]
        }

        Emitter {
            id: emitter
            system: sys
            enabled: false              // 先关闭
            lifeSpan: 8000
            maximumEmitted: 4000
            anchors.fill: parent
            size: 16
            acceleration: PointDirection { xVariation: 12; yVariation: 12 }  // 向四周发散
        }
        MouseArea {
            anchors.fill: parent           // 点击使能
            onClicked: emitter.burst(4000);
        }
    }
}