9. Shader Effect
注意
最后一次构建:2014年1月20日下午18:00。
这章的源代码能够在http://qmlbook.org/assets/中找到。
着色器允许我们利用SceneGraph的接口直接调用在强大的GPU上运行的OpenGL来创建渲染效果。着色器使用ShaderEffect与ShaderEffectSource元素来实现。着色器本身的算法使用OpenGL Shading Language(OpenGL着色语言)来实现。
实际上这意味着你需要混合使用QML代码与着色器代码。执行时,会将着色器代码发送到GPU,并在GPU上编译执行。QML着色器元素(Shader QML Elements)允许你与OpenGL着色器程序的属性交互。
让我们首先来看看OpenGL着色器。
9.1 OpenGL着色器(OpenGL Shader)
OpenGL的渲染管线分为几个步骤。一个简单的OpenGL渲染管线将包含一个顶点着色器和一个片段着色器。
顶点着色器接收顶点数据,并且在程序最后赋值给gl_Position。然后,顶点将会被裁剪,转换和栅格化后作为像素输出。片段(像素)进入片段着色器,进一步对片段操作并将结果的颜色赋值给gl_FragColor。顶点着色器调用多边形每个角的点(顶点=3D中的点),负责这些点的3D处理。片段(片度=像素)着色器调用每个像素并决定这个像素的颜色。
9.2 着色器元素(Shader Elements)
为了对着色器编程,Qt Quick提供了两个元素。ShaderEffectSource与ShaderEffect。ShaderEffect将会使用自定义的着色器,ShaderEffectSource可以将一个QML元素渲染为一个纹理然后再渲染这个纹理。由于ShaderEffect能够应用自定义的着色器到它的矩形几何形状,并且能够使用在着色器中操作资源。一个资源可以是一个图片,它被作为一个纹理或者着色器资源。
默认下着色器使用这个资源并且不作任何改变进行渲染。
在上边这个例子中,我们在一行中显示了3张图片,第一张是原始图片,第二张使用默认的着色器渲染出来的图片,第三张使用了Qt5源码中默认的顶点与片段着色器的代码进行渲染的图片。
注意
如果你不想看到原始图片,而只想看到被着色器渲染后的图片,你可以设置Image为不可见(visible:false)。着色器仍然会使用图片数据,但是图像元素(Image Element)将不会被渲染。
让我们仔细看看着色器代码。
着色器代码来自Qt这边的一个字符串,绑定了顶点着色器(vertexShader)与片段着色器(fragmentShader)属性。每个着色器代码必须有一个main(){....}函数,它将被GPU执行。Qt已经默认提供了以qt_开头的变量。
下面是这些变量简短的介绍:
- uniform-在处理过程中不能够改变的值。
- attribute-连接外部数据
- varying-着色器之间的共享数据
- highp-高精度值
- lowp-低精度值
- mat4-4x4浮点数(float)矩阵
- vec2-包含两个浮点数的向量
- sampler2D-2D纹理
- float-浮点数
可以查看http://www.khronos.org/opengles/sdk/docs/reference_cards/OpenGL-ES-2_0-Reference-card.pdf获得更多信息。
现在我们可以更好的理解下面这些变量:
- qt_Matrix:model-view-projection(模型-视图-投影)矩阵
- qt_Vertex:当前顶点坐标
- qt_MultiTexCoord0:纹理坐标
- qt_TexCoord0:共享纹理坐标
我们已经有可以使用的投影矩阵(projection matrix),当前顶点与纹理坐标。纹理坐标与作为资源(source)的纹理相关。在main()函数中,我们保存纹理坐标,留在后面的片段着色器中使用。每个顶点着色器都需要赋值给gl_Postion,在这里使用项目矩阵乘以顶点,得到我们3D坐标系中的点。
片段着色器从顶点着色器中接收我们的纹理坐标,这个纹理仍然来自我们的QML资源属性(source property)。在着色器代码与QML之间传递变量是如此的简单。此外我们的透明值,在着色器中也可以使用,变量是qt_Opacity。每个片段着色器需要给gl_FragColor变量赋值,在这里默认着色器代码使用资源纹理(source texture)的像素颜色与透明值相乘。
在后面的例子中,我们将会展示一些简单的着色器例子。首先我们会集中在片段着色器上,然后在回到顶点着色器上。
9.3 片段着色器(Fragement Shader)
片段着色器调用每个需要渲染的像素。我们将开发一个红色透镜,它将会增加图片的红色通道的值。
配置场景(Setting up the scene)
首先我们配置我们的场景,在区域中央使用一个网格显示我们的源图片(source image)。
红色着色器(A red Shader)
下一步我们添加一个着色器,显示一个红色矩形框。由于我们不需要纹理,我们从顶点着色器中移除纹理。
在片段着色器中,我们简单的给gl_FragColor赋值为vec4(1.0, 0.0, 0.0, 1.0),它代表红色, 并且不透明(alpha=1.0)。
使用纹理的红色着色器(A red shader with texture)
现在我们想要将这个红色应用在纹理的每个像素上。我们需要将纹理加回顶点着色器。由于我们不再在顶点着色器中做任何其它的事情,所以默认的顶点着色器已经满足我们的要求。
完整的着色器重新包含我们的源图片作为属性,由于我们没有特殊指定,使用默认的顶点着色器,我没有重写顶点着色器。
在片段着色器中,我们提取纹理片段texture2D(source,qt_TexCoord0),并且与红色一起应用。
红色通道属性(The red channel property)
这样的代码用来修改红色通道的值看起来不是很好,所以我们想要将这个值包含在QML这边。我们在ShaderEffect中增加一个redChannel属性,并在我们的片段着色器中申明一个uniform lowpfloat redChannel。这就是从一个着色器代码中标记一个值到QML这边的方法,非常简单。
为了让这个透镜更真实,我们改变vec4颜色为vec4(redChannel, 1.0, 1.0, 1.0),这样其它颜色与1.0相乘,只有红色部分使用我们的redChannel变量。
红色通道的动画(The red channel animated)
由于redChannel属性仅仅是一个正常的属性,我们也可以像其它QML中的属性一样使用动画。我们使用QML属性在GPU上改变这个值,来影响我们的着色器,这真酷!
下面是最后的结果。
在这4秒内,第二排的着色器红色通道的值从0.0到1.0。图片从没有红色信息(0.0 red)到一个正常的图片(1.0 red)。
9.4 波浪效果(Wave Effect)
在这个更加复杂的例子中,我们使用片段着色器创建一个波浪效果。波浪的形成是基于sin曲线,并且它影响了使用的纹理坐标的颜色。
波浪的计算是基于一个脉冲与纹理坐标的操作。我们使用一个基于当前时间与使用的纹理坐标的sin波浪方程式来实现脉冲。
离开了时间的因素,我们仅仅只有扭曲,而不是像波浪一样运动的扭曲。
我们使用不同的纹理坐标作为颜色。
纹理坐标受我们的x脉冲值影响,结果就像一个移动的波浪。
如果我们没有在片段着色器中使用像素的移动,这个效果可以首先考虑使用顶点着色器来完成。
9.5 顶点着色器(Vertex Shader)
顶点着色器用来操作ShaderEffect提供的顶点。正常情况下,ShaderEffect有4个顶点(左上top-left,右上top-right,左下bottom-left,右下bottom-right)。每个顶点使用vec4类型记录。为了实现顶点着色器的可视化,我们将编写一个吸收的效果。这个效果通常被用来让一个矩形窗口消失为一个点。
配置场景(Setting up the scene)
首先我们再一次配置场景。
这个场景使用了一个黑色背景,并且提供了一个使用图片作为资源纹理的ShaderEffect。使用image元素的原图片是不可见的,只是给我们的吸收效果提供资源。此外我们在ShaderEffect的位置添加了一个同样大小的黑色矩形框,这样我们可以更加明确的知道我们需要点击哪里来重置效果。
点击图片将会触发效果,MouseArea覆盖了ShaderEffect。在onClicked操作中,我们绑定了自定义的布尔变量属性minimized。我们稍后使用这个属性来触发效果。
最小化与正常化(Minimize and normalize)
在我们配置好场景后,我们定义一个real类型的属性,叫做minimize,这个属性包含了我们当前最小化的值。这个值在0.0到1.0之间,由一个连续的动画来控制它。
这个动画绑定了由minimized属性触发。现在我们已经配置好我们的环境,最后让我们看看顶点着色器的代码。
顶点着色器被每个顶点调用,在我们这个例子中,一共调用了四次。默认下提供qt已定义的参数,如qt_Matrix,qt_Vertex,qt_MultiTexCoord0,qt_TexCoord0。我们在之前已经讨论过这些变量。此外我们从ShaderEffect中链接minimize,width与height的值到我们的顶点着色器代码中。在main函数中,我们将当前纹理值保存在qt_TexCoord()中,让它在片段着色器中可用。现在我们拷贝当前位置,并修改顶点的x,y的位置。
mix(...)函数提供了一种在两个参数之间(0.0到1.0)的线性插值的算法。在我们的例子中,在当前y值与高度值之间基于minimize的值插值获得y值,x的值获取类似。记住minimize的值是由我们的连续动画控制,并且在0.0到1.0之间(反之亦然)。
这个结果的效果不是真正吸收效果,但是已经能朝着这个目标完成了一大步。
基础弯曲(Primitive Bending)
我们已经完成了最小化我们的坐标。现在我们想要修改一下对x值的操作,让它依赖当前的y值。这个改变很简单。y值计算在前。x值的插值基于当前顶点的y坐标。
这个结果造成当y值比较大时,x的位置更靠近width的值。也就是说上面2个顶点根本不受影响,它们的y值始终为0,下面两个顶点的x坐标值更靠近width的值,它们最后转向同一个x值。
更好的弯曲(Better Bending)
现在简单的弯曲并不能真正的满足我们的要求,我们将添加几个部件来提升它的效果。首先我们增加动画,支持一个自定义的弯曲属性。这是非常必要的,由于弯曲立即发生,y值的最小化需要被推迟。两个动画在同一持续时间计算总和(300+700+100与700+1300)。
此外,为了使弯曲更加平滑,不再使用y值影响x值的弯曲函数,pos.x现在依赖新的弯曲属性动画:
弯曲从0.0平滑开始,逐渐加快,在1.0时逐渐平滑。下面是这个函数在指定范围内的曲线图。对于我们,只需要关注0到1的区间。
想要获得最大化的视觉改变,需要增加我们的顶点数量。可以使用网眼(mesh)来增加顶点:
现在ShaderEffect被分布为16x16顶点的网格,替换了之前2x2的顶点。这样顶点之间的插值将会看起来更加平滑。
你可以看见曲线的变化,在最后让弯曲变得非常平滑。这让弯曲有了更加强大的效果。
侧面收缩(Choosing Sides)
最后一个增强,我们希望能够收缩边界。边界朝着吸收的点消失。直到现在它总是在朝着width值的点消失。添加一个边界属性,我们能够修改这个点在0到width之间。
包装(Packing)
最后将我们的效果包装起来。将我们吸收效果的代码提取到一个叫做GenieEffect的自定义组件中。它使用ShaderEffect作为根元素。移除掉MouseArea,这不应该放在组件中。绑定minimized属性来触发效果。
你现在可以像这样简单的使用这个效果:
我们简化了代码,移除了背景矩形框,直接使用图片完成效果,替换了在一个单独的图像元素中加载它。
9.6 剧幕效果(Curtain Effect)
在最后的自定义效果例子中,我们将带来一个剧幕效果。这个效果是2011年5月Qt实验室发布的着色器效果中的一部分。目前网址已经转到blog.qt.digia.com,不知道还能不能找到。
当时我非常喜欢这些效果,剧幕效果是我最喜爱的一个。我喜欢剧幕打开然后遮挡后面的背景对象。
我将代码移植适配到Qt5上,这非常简单。同时我做了一些简化让它能够更好的展示。如果你对整个例子有兴趣,可以访问Qt实验室的博客。
只有一个小组件作为背景,剧幕实际上是一张图片,叫做fabric.jpg,它是ShaderEffect的资源。整个效果使用顶点着色器来摆动剧幕,使用片段着色器提供阴影的效果。下面是一个简单的图片,让你更加容易理解代码。
剧幕的波形阴影通过一个在剧幕宽度上的sin曲线使用7的振幅来计算(7*PI=221.99..)另一个重要的部分是摆动,当剧幕打开或者关闭时,使用动画来播放剧幕的topWidth。bottomWidth使用SpringAnimation来跟随topWidth变化。这样我们就能创建出底部摆动的剧幕效果。计算得到的swing提供了摇摆的强度,用来对顶点的y值进行插值。
剧幕效果放在CurtainEffect.qml组件中,fabric图像作为纹理资源。在阴影的使用上没有新的东西加入,唯一不同的是在顶点着色器中操作gl_Postion和片段着色器中操作gl_FragColor。
这个效果在curtaindemo.qml文件中使用。
剧幕效果通过自定义的open属性打开。我们使用了一个MouseArea来触发打开和关闭剧幕。
9.7 Qt图像效果库(Qt GraphicsEffect Library)
图像效果库是一个着色器效果的集合,是由Qt开发者提供制作的。它是一个很好的工具,你可以将它应用在你的程序中,它也是一个学习如何创建着色器的例子。
图像效果库附带了一个手动测试平台,这个工具可以帮助你测试发现不同的效果
测试工具在$QTDIR/qtgraphicaleffects/tests/manual/testbed下。
效果库包含了大约20种效果,下面是效果列表和一些简短的描述。
种类 | 效果 | 描述 |
---|---|---|
混合(Blend) | 混合(Blend) | 使用混合模式合并两个资源项 |
颜色(Color) | 亮度与对比度(BrightnessContrast) | 调整亮度与对比度 |
着色(Colorize) | 设置HSL颜色空间颜色 | |
颜色叠加(ColorOverlay) | 应用一个颜色层 | |
降低饱和度(Desaturate) | 减少颜色饱和度 | |
伽马调整(GammaAdjust) | 调整发光度 | |
色调饱和度(HueSaturation) | 调整HSL颜色空间颜色 | |
色阶调整(LevelAdjust) | 调整RGB颜色空间颜色 | |
渐变(Gradient) | 圆锥渐变(ConicalGradient) | 绘制一个圆锥渐变 |
线性渐变(LinearGradient) | 绘制一个线性渐变 | |
射线渐变(RadialGradient) | 绘制一个射线渐变 | |
失真(Distortion) | 置换(Displace) | 按照指定的置换源移动源项的像素 |
阴影(Drop Shadow) | 阴影 (DropShadow) | 绘制一个阴影 |
内阴影(InnerShadow) | 绘制一个内阴影 | |
模糊 (Blur) | 快速模糊(FastBlur) | 应用一个快速模糊效果 |
高斯模糊(GaussianBlur) | 应用一个高质量模糊效果 | |
蒙版模糊(MaskedBlur) | 应用一个多种强度的模糊效果 | |
递归模糊(RecursiveBlur) | 重复模糊,提供一个更强的模糊效果 | |
运动模糊(Motion Blur) | 方向模糊(DirectionalBlur) | 应用一个方向的运动模糊效果 |
放射模糊(RadialBlur) | 应用一个放射运动模糊效果 | |
变焦模糊(ZoomBlur) | 应用一个变焦运动模糊效果 | |
发光(Glow) | 发光(Glow) | 绘制一个外发光效果 |
矩形发光(RectangularGlow) | 绘制一个矩形外发光效果 | |
蒙版(Mask) | 透明蒙版(OpacityMask) | 使用一个源项遮挡另一个源项 |
阈值蒙版(ThresholdMask) | 使用一个阈值,一个源项遮挡另一个源项 |
下面是一个使用快速模糊效果的例子:
左边是原图片。点击右边的图片将会触发blurred属性,模糊在1秒内从0到32。左边显示模糊后的图片。