本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873) 接上文[Qt5官方demo解析集11——Qt Quick Particles Examples - Affectors](http://blog.csdn.net/cloud_castle/article/details/33723715) 使用Emitter和Affectors强大的功能,我们已经可以构造出丰富多彩的粒子特效,但当这些功能还不能满足我们的需要时,我们可以转而采用CustomParticle取代ImageParticle,在CustomParticle中我们可以使用基于GLSL的渲染技术来创建自定义的粒子。 这个demo依然由一些小例子组成,不过比前两个demo中的都要少,只有三个: ![](https://box.kancloud.cn/2016-01-18_569cbd04019da.png) (1)Blur Particles 在这个例子中我们可以看到使用CustomParticle创建模糊化粒子的方法。运行效果如下: ![](https://box.kancloud.cn/2016-01-18_569cbd0414fbc.jpg) 个人感觉在这个演示中模糊效果并不是很清楚,于是换了一张图片,并类似地使用不带模糊化的ImageParticle来展示它们的不同: ![](https://box.kancloud.cn/2016-01-18_569cbd042de54.jpg) 可以看到,左边图为模糊化的CustomParticle,右边为没有模糊效果的ImageParticle,区别还是很明显的。 由于笔者对OpenGL并不是很熟悉,如果有错误的地方,还请各位指正。 下面的部分内容来自[http://qmlbook.org/ch09/index.html](http://qmlbook.org/ch09/index.html), 在此表示感谢。 为了更深刻理解CustomParticle中的顶点着色器与片元着色器,我们先看一段这两个着色器基本的代码: ~~~ // default vertex shader code vertexShader: " uniform highp mat4 qt_Matrix; attribute highp vec4 qt_Vertex; attribute highp vec2 qt_MultiTexCoord0; varying highp vec2 qt_TexCoord0; void main() { qt_TexCoord0 = qt_MultiTexCoord0; gl_Position = qt_Matrix * qt_Vertex; }" // default fragment shader code fragmentShader: " varying highp vec2 qt_TexCoord0; uniform sampler2D source; uniform lowp float qt_Opacity; void main() { gl_FragColor = texture2D(source, qt_TexCoord0) * qt_Opacity; }" ~~~ 其中,vertexShader得到qt_TexCoord0纹理坐标供fragmentShader使用; gl_Position为图元各个坐标点的输出; texture2D()通过qt_TexCoord0从source中取得纹理值,乘上透明度作为gl_FragColor的片元颜色输出。 注:如果不写vertexShader,Qt将采用默认的顶点渲染器,即各点与图像本身的位置不变。反之也是一样。 可以看到我们定义了很多qt_XXX的变量,但它们并没有被赋值。 实际上这是Qt为我们提供了一些提前定义好的变量。但是既然Qt已经定义了,我们为什么又要定义一遍呢? 这就涉及到QML与GLSL之间的相互映射了,不同与其他QML元素,着色器中的GLSL语句是不能够直接使用QML所定义的属性变量的。 但是QML中任何类型的属性都可以通过在着色器语句中声明相同名称的属性来映射到GLSL的变量中去。其类型一般为uniform或attribute。 也就是说,当我们在QML中定义了一个4维RGBA的QColor color,然后在GLSL中声明一个uniform vec4 color,一个映射就被搭建起来了。 要知道QML中的绝大多数操作都是基于属性完成的,那么通过这种映射,我们能够很容易地创建丰富多彩的GLSL显示效果。 我们用一个小例子来阐述这一点,注意redChannel属性与source在fragmentShader中的使用: ~~~ ShaderEffect { id: effect3 width: 80; height: width property variant source: sourceImage property real redChannel: 0.3 visible: root.step>2 fragmentShader: " varying highp vec2 qt_TexCoord0; uniform sampler2D source; uniform lowp float qt_Opacity; uniform lowp float redChannel; void main() { gl_FragColor = texture2D(source, qt_TexCoord0) * vec4(redChannel, 1.0, 1.0, 1.0) * qt_Opacity; }" } ~~~ 我们也可以将类型声明为var,Qt会自动帮我们完成转换。下面的例子中大量使用了这种方法。 qt 预定义的变量均以qt_XXX这种格式来表示,比如 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)等等。 由于两个着色器的main(){}函数都是在GPU中执行的,这大大提升了我们的图形渲染速度。 下面是GLSL的变量类型定义: | uniform | value does not change during processing | |--|--| | attribute | linkage to external data | | varying | shared value between shaders | | highp | high precision value | | lowp | low precision value | | mat4 | 4x4 float matrix | | vec2 | 2=dim float vector | | sampler2D | 2D texture | | float | floating scalar | 详细的类型映射可以参考Manual中的ShaderEffect。 更多的信息可以参考[OpenGL ES 2.0 API Quick Reference Card](http://www.khronos.org/opengles/sdk/docs/reference_cards/OpenGL-ES-2_0-Reference-card.pdf) 看源码时,我们先看一段额外的代码,我们将其命名为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:" // 这里是我们的第一个顶点着色器 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 向我们介绍了如何直接使用片元着色器来绘制粒子。 ![](https://box.kancloud.cn/2016-01-18_569cbd0453563.jpg) 屏幕下方的话也揭示了这个例子的主题。 这个例子的层次的结构没有上一个例子那么复杂,来看看吧,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覆盖图像不同,这里的粒子颜色会随着图片中当前像素的变化而变化。 ![](https://box.kancloud.cn/2016-01-18_569cbd0471515.jpg) 在屏幕上点击过后,图像会以粒子的形态展现出来,然后向四周发散。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); } } } ~~~