![gallery-img](https://i-blog.csdnimg.cn/blog_migrate/076cd3a5baf2dd62627a8a0f0e3036bf.png)
简介
这次涛哥将会教大家一些ShaderEffect(参考QmlBook,译作:着色器效果)的相关知识。
前面的文章,给大家展示了进场动画,以及页面切换动画,大部分都使用了ShaderEffect,所以这次专门来说一下ShaderEffect。
源码
《玩转Qml》系列文章,配套了一个优秀的开源项目:TaoQuick
github https://github.com/jaredtao/TaoQuick
访问不了或者速度太慢,可以用国内的镜像网站gitee
https://gitee.com/jaredtao/TaoQuick
ShaderEffect
动画只能控制组件的属性整体的变化,做特效需要精确到像素。
Qml中提供了ShaderEffect这个组件,就能实现像素级别的操作。
ShaderEffect允许我们在Qml的渲染引擎SceneGraph上,利用强大的GPU进行渲染。
使用ShaderEffect,需要有一些图形学知识,了解GPU渲染管线,了解图形API如OpenGL、DirectX等,同时也需要一些数学知识。
图形学的知识体系还是非常庞大的,要系统的学习,需要看很多书籍。入门级的比如“红宝书”《OpenGL编程指南》、“蓝宝书”《OpenGL超级宝典》……
一篇文章是说不完的,涛哥水平也有限。所以本文从实用的角度出发,按照涛哥自己的理解,提炼一些必要的知识点,省略一些无关的细节,
让各位Qt开发者能了解GPU原理,能看懂、甚至于自己写一些简单的着色器代码,就大功告成了。说的不对的地方,也欢迎大佬来指点。
显示器如何显示色彩
先来了解一下,显示器是如何显示出各种色彩的。
假如我们把显示器的画面放大100倍,就会看到很多整齐排列的像素点。
![](https://i-blog.csdnimg.cn/blog_migrate/80dd3893d53dc91463f274a01ecf3953.png)
继续放大,就会发现每个像素点,由三种发光的元件组成,这三种元件分别发出红、绿、蓝三种颜色的光。三种颜色的光组合在一起,
就是人眼看到的颜色。这就是著名的RGB颜色模型。
![](https://i-blog.csdnimg.cn/blog_migrate/e8c0a29c84d03a379487d700906afa18.png)
如果把这三种光的亮度分为255个等级,就能组合出16777216种不同颜色的光。
GPU的任务,就是通过计算,给出每一个像素的红、绿、蓝 (简称r g b)三种颜色的数值,让显示器去”发出相应的光”。
(这样说可能不太严谨、不太专业,只是方便大家理解。另一方面,本文的目的,
是让大家学习如何写特效,不是去造显卡/造显示器。所以请专业人士见谅!)
注:参考[1]
GPU渲染流程
我们以画一个填充色的三角形为例,来说明
![](https://i-blog.csdnimg.cn/blog_migrate/c01ea0c45a5b5f438db1d4ddf0476eab.png)
渲染管线图
下图是一个简易的渲染管线,引用自 LearnOpenGL
![](https://i-blog.csdnimg.cn/blog_migrate/e93611ae8fc785cbee940b915780cb68.png)
画一个三角形,要经历顶点着色器、图元装配、几何着色器、片段着色器、光栅化等阶段。
其中蓝色部分是可以自定义的,自定义是指,按照图形API规范,写一段GPU能编译、运行的代码。
(这种代码就是着色器代码。可以自定义的这种渲染管线,就是可编程渲染管线,与之相对的是古老的固定渲染管线。)
这里各个阶段,分别引用一下,LearnOpenGL中的介绍(看不懂可以先跳过,看我画的图):
1 管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是
把3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。
2 图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),
并所有的点装配成指定图元的形状;本节例子中是一个三角形。
3 图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,
它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。
4 几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,
生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。
裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
5 片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包
含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
并行管线示意图
概念还是挺多的,而且很多教程都有渲染管线图。但是涛哥觉得,对于我们开发Shader来说,一定要有并行的意识,然而大部分
管线图,都没有体现出GPU的并行特性。所以涛哥自己画了一个草图:
![](https://i-blog.csdnimg.cn/blog_migrate/cc33ef1fb782790e292ab1576f33b716.png)
解释一下吧,CPU传入了3个顶点到GPU,GPU将这三个顶点,传递给三个顶点着色器。
这里要意识到,顶点着色器开始,就是并行处理了。GPU是很强大的SIMD架构(单指令流多数据流)。
如果我们自定义了一段顶点着色器代码,则三个顶点会同时运行这段代码。(后面的片段着色器代码,就是N个点同时运行)
顶点着色器进行处理,传递给图元装配。
图元装配阶段,进行了顶点扩充,变成N个点,N看作三角形面积所在的点。
之后N个点依次传给 几何着色器->光栅化->片段着色器,最后经过测试与混合后,输出到屏幕。
可以自定义编程的,有顶点着色器、几何着色器、片段着色器(有的地方也叫像素着色器),顺带提一下,还有另外三种:
曲面控制着色器、曲面评估着色器 和 计算着色器。
一般我们的关注点,都会在片段着色器上。涛哥之前写的12种特效,就只用了自定义的片段着色器。
著名的ShaderToy网站,也是只关注片段着色器。ShaderToy
着色器语言编码规范
我们可以把着色器语言,当作运行在GPU上的C语言。
Qt的ShaderEffect支持的着色器语言包括OpenGL规范中的GLSL,和DirectX规范中的HLSL,这两种着色语法上有些细微的区别,但是可以互相转换。
我们就以glsl为主。详细的语言规范,在khronos的官网, 各个版本都有: https://www.khronos.org/registry/OpenGL/specs/gl/
![](https://i-blog.csdnimg.cn/blog_migrate/563dcca44fb311343d240f132cb98e18.png)
桌面版 OpenGL 版本众多,而嵌入式系统也有专用的OpenGL ES。
安卓手机、平板设备一般就是OpenGL ES,新的设备都支持ES 3.0,老的设备一般只支持到ES 2.0
OpenGL ES 的语言规范文档在这里: https://www.khronos.org/registry/OpenGL/specs/es/2.0/
我们就用Qt默认的版本。
着色器代码示例
示例
这里用Qt帮助文档中的示例代码,来说明。
import QtQuick 2.0
Rectangle {
width: 200; height: 100
Row {
Image { id: img;
sourceSize { width: 100; height: 100 } source: "qt-logo.png" }
ShaderEffect {
width: 100; height: 100
property variant src: img
vertexShader: "
uniform highp mat4 qt_Matrix;
attribute highp vec4 qt_Vertex;
attribute highp vec2 qt_MultiTexCoord0;
varying highp vec2 coord;
void main() {
coord = qt_MultiTexCoord0;
gl_Position = qt_Matrix * qt_Vertex;
}"
fragmentShader: "
varying highp vec2 coord;
uniform sampler2D src;
uniform lowp float qt_Opacity;
void main() {
lowp vec4 tex = texture2D(src, coord);
gl_FragColor = vec4(vec3(dot(tex.rgb,
vec3(0.344, 0.5, 0.156))),
tex.a) * qt_Opacity;
}"
}
}
}
这段代码的效果是
![](https://i-blog.csdnimg.cn/blog_migrate/03de58a7a5ddf77492632c102b1812a0.png)
左边是本来的绿色的Qt的logo,右边是处理过后的灰色logo。
着色器代码
ShaderEffect的vertexShader属性就是顶点着色器了,其内容是一段字符串。按照着色器规范实现的。
同样的,fragmentShader属性 即片段着色器。
我们能在着色器中看到void main函数,这个便是着色器代码的入口函数,和C语言很像。
在main之前,还有一些全局变量,我们逐条来说明一下
在顶点着色器中,有这三种不同用处的变量:uniform、attribute、varying。
这些变量的值都是从CPU传递过来的。
如果你写过原生OpenGL的代码,就会知道,其中很大一部分工作,就是在处理CPU数据传递到GPU着色器中。
而Qml的ShaderEffect简化了这些工作,只要写一个property,名字、类型和着色器中的对应上,就可以了。
顶点着色器
attribute highp vec4 qt_Vertex;
attribute是”属性”变量,按照前面涛哥画的管线图来说,三个顶点着色器同时运行时,每个着色器中
的attribute值都不一样。这里的qt_Vertex,可以理解为分别是三角形的三个顶点。
highp是精度限定符,这里先忽略,具体细节可以参考语言规范文档。后面的lowp、 medium也是精度限定符。
vec4就是四维向量,类似QVector4D。
qt_Vertex是变量的名字。
这条语句的作用,就是声明一个用来存储顶点的attribute变量qt_Vertex。
uniform是统一变量,三个顶点着色器同时运行时,它们取得的uniform变量值是一样的。
varying表示这个顶点着色器的输出数据,将传递给后面的渲染管线。
void main()
{
coord = qt_MultiTexCoord0;
gl_Position = qt_Matrix * qt_Vertex;
}
这段main函数,将CPU传进来的纹理坐标qt_MultiTexCoord0数据,通过varying变量coord,传递给了下一个阶段,然后使用矩阵进行了坐标转换,
并将结果存储在glsl的内置变量gl_Position中。
片段着色器
片段着色器中,就没有attribute了。uniform是一样的统一变量,varying是上一个阶段传递进来的数据。
uniform sampler2D src;
sampler2D是二维纹理。所谓纹理嘛,可以理解成一张图片,一个Image。
src这个变量,就代表外面传进来的那个Image。 sampler2D也可以是任意可视的Item(通过ShaderEffectSource传递进来)
来看一下main函数
void main()
{
lowp vec4 tex = texture2D(src, coord);
gl_FragColor = vec4(vec3(dot(tex.rgb,vec3(0.344, 0.5, 0.156))), tex.a) * qt_Opacity;
}
这里使用了纹理
lowp vec4 tex = texture2D(src, coord);
texture2D是一个内置函数,专业术语叫“对纹理进行采样”,什么意思呢?
假如coord的值是(0,0),那就是对src指代的这张图片,取x=0、y=0的坐标点的像素,作为返回值,存储在tex变量中。
这里注意一下纹理坐标的取值范围。假如Qml中图片的大小是100x100,其取值范围从(0, 0) -> (100, 100)
这里的传进来的纹理坐标,取值范围是(0, 0) -> (1, 1) ,GPU为了方便计算,都进行了归1化处理。将范围缩小到0 - 1
gl_FragColor = vec4(vec3(dot(tex.rgb, vec3(0.344, 0.5, 0.156) )), tex.a) * qt_Opacity;
dot(tex.rgb, vec3(0.344, 0.5, 0.156) ) 是对两个三维向量进行了点乘。
tex.rgb是GLSL中的取值器语法。 tex是一个四维变量,可以用tex.r tex.g tex.b tex.a分别取出其中一维,也可以任意两个组合、三个
组合取值。
rgba可以取值,xyzw也可以取值, stpq也行,但只能三种选一种,不能混用。
vec4(vec3(), tex.a) 是用三维向量再加一个变量,构造四维向量。
这条语句其实是一个RGB转灰度的公式,可以自行搜索相关的资料。
gl_FragColor 是内置变量,表示所在片段着色器的最终的输出颜色。