9.着色器渲染(Shader Effects)
本章的作者:jryannel
** 注意: **
最新的构建时间:2016/03/21
这章的源代码能够在assetts folder找到。
下列内容:
http://doc.qt.io/qt-5/qml-qtquick-shadereffect.html
http://www.opengl.org/registry/doc/GLSLangSpec.4.20.6.clean.pdf
http://www.khronos.org/registry/gles/specs/2.0/GLSL_ES_Specification_1.0.17.pdf
http://www.lighthouse3d.com/opengl/glsl/
http://wiki.delphigl.com/index.php/Tutorial_glsl
http://doc.qt.io/qt-5/qtquick-shadereffects-example.html
简要介绍着色器效果,然后呈现着色效果及其使用。
着色器允许我们在 SceneGraph API 上创建出强大的渲染效果,直接利用在 GPU 上运行的 OpenGL 的强大功能。着色器使用 ShaderEffect 和
ShaderEffectSource 元素实现。着色器算法本身使用 OpenGL 着色语言实现。
实际上这意味着我们将 QML 代码与着色器代码混合在一起。执行时将着色器代码发送到 GPU,并在 GPU 上编译并执行。着色器 QML 元素允许我们通过属性与 OpenGL 着色器实现进行交互。
我们先来看看 OpenGL 着色器是什么。
9.1 OpenGL 着色器
OpenGL 使用渲染流水线分割成几个阶段。一个简化的 OpenGL 管道将包含一个顶点和片段着色器。
顶点着色器接收顶点数据,必须将其分配给程序结束时的 gl_Position。在下一阶段,顶点被剪切,变换和光栅化以进行像素输出。从那里,片段(像素)到达片段着色器,并且可以进一步被操纵,并且所得到的颜色需要被分配给 gl_FragColor。顶点着色器被称为我们的多边形的每个角点(顶点= 3D 中的点),并且负责对这些点的任何 3D 操纵。为每个像素调用片段(fragment = pixel)着色器,并确定该像素的颜色。
9.2 着色器元素
对于编程着色器 Qt Quick 提供了两个元素。ShaderEffectSource 和 ShaderEffect。着色器效果应用自定义着色器,着色器效果源将 QML 项呈现到纹理中并呈现。由于着色效果可以将自定义着色器应用于其矩形形状,并可使用源作为着色器操作。源可以是用作纹理或着色器效果源的图像。
下面是默认着色器使用的源代码,我们对其进行了简单的修改:
import QtQuick 2.5
Rectangle {
width: 480; height: 240
color: '#1e1e1e'
Row {
anchors.centerIn: parent
spacing: 20
Image {
id: sourceImage
width: 80; height: width
source: 'assets/tulips.jpg'
}
ShaderEffect {
id: effect
width: 80; height: width
property variant source: sourceImage
}
ShaderEffect {
id: effect2
width: 80; height: width
// the source where the effect shall be applied to
property variant source: sourceImage
// 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;
}"
}
}
}
在上面的例子中,我们有一排3张图像。第一个是真实的形象。第二个使用默认着色器渲染,第三个渲染使用从 Qt 5 源代码提取的片段和顶点的默认着色器代码。
** 注意: **
如果我们不想看到源图片,而只想看到被着色器渲染后的图片,我们可以通过(visible:false)设置 Image 为不可见。着色器仍然会使用图片数据,但是源图像元素将不会被渲染和显示。
我们来看看着色器代码。
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;
}"
两个着色器都是从 Qt 端到一个绑定到 vertexShader 和 fragmentShader 属性的字符串。每个着色器代码必须有一个 main(){...} 函数,由 GPU 执行。默认情况下,由 qt_ 开始的变量已经由 Qt 提供。
这里有一个简短的变量集:
变量 | 简介 |
---|---|
uniform | 值在处理过程中不会改变 |
attribute | 链接到外部数据 |
varying | 着色器之间的共享值 |
highp | 高精度 |
lowp | 低精度 |
mat4 | 4x4 浮点矩阵 |
vec2 | 2=dim 浮点矢量 |
sampler2D | 2D 纹理 |
float | 浮动的标量 |
更好的参考是 OpenGL ES 2.0 API 快速参考卡
现在我们可以更好地了解变量是什么意思了:
- qt_Matrix: 模型视图投影(model-view-projection)矩阵
- qt_Vertex: 当前顶点位置
- qt_MultiTexCoord0: 纹理坐标
- qt_TexCoord0: 共享纹理坐标
所以我们可以使用投影矩阵,当前顶点和纹理坐标。纹理坐标与作为源的纹理相关。在 main() 函数中,我们存储纹理坐标以供以后在片段着色器中使用。每个顶点着色器需要分配 gl_Position,这是通过将项目矩阵与顶点相乘来完成的,我们的点在 3D 中。
片段着色器从顶点着色器接收我们的纹理坐标,并从 QML 源属性接收纹理。应注意,在着色器代码和 QML 之间传递变量是多么容易和优雅。另外,我们有着色器效果的不透明度可用作 qt_Opacity。每个片段着色器需要分配 gl_FragColor 变量,这是通过从源纹理中选择像素并将其与不透明度相乘来在默认着色器代码中完成的。
fragmentShader: "
varying highp vec2 qt_TexCoord0;
uniform sampler2D source;
uniform lowp float qt_Opacity;
void main() {
gl_FragColor = texture2D(source, qt_TexCoord0) * qt_Opacity;
}"
在接下来的例子中,我们将使用一些简单的着色器技巧。首先我们专注于片段着色器,然后我们再回到顶点着色器。
9.3 片段着色器
每个像素都要渲染片段着色器。我们将开发一个小红色镜头,这将增加图像的红色通道值。
** 设置场景 **
首先我们设置我们的场景,一个以场为中心的网格,并显示我们的源图像。
import QtQuick 2.5
Rectangle {
width: 480; height: 240
color: '#1e1e1e'
Grid {
anchors.centerIn: parent
spacing: 20
rows: 2; columns: 4
Image {
id: sourceImage
width: 80; height: width
source: 'assets/tulips.jpg'
}
}
}
** 红色着色器 **
接下来,我们将添加一个着色器,它通过为每个片段提供一个红色的颜色值来显示一个红色的矩形。
fragmentShader: "
uniform lowp float qt_Opacity;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0) * qt_Opacity;
}"
在片段着色器中,我们简单地为每个片段分配一个表示具有完全不透明度(alpha = 1.0)的红色的 vec4(1.0,0.0,0.0,1.0)到 gl_FragColor 属性。
** 带有纹理的红色着色器 **
现在我们要将红色应用于每个纹理像素。为此,我们需要纹理返回顶点着色器。由于我们在顶点着色器中没有做任何其他事情,所以默认顶点着色器对我们来说足够了。
ShaderEffect {
id: effect2
width: 80; height: width
property variant source: sourceImage
visible: root.step>1
fragmentShader: "
varying highp vec2 qt_TexCoord0;
uniform sampler2D source;
uniform lowp float qt_Opacity;
void main() {
gl_FragColor = texture2D(source, qt_TexCoord0) * vec4(1.0, 0.0, 0.0, 1.0) * qt_Opacity;
}"
}
完整的着色器现在将我们的图像源作为 variant 属性返回,我们已经省略了顶点着色器,如果没有指定,则使用默认顶点着色器。
在片段着色器中,我们选择纹理片段 texture2D(source,qt_TexCoord0) 并将红色应用于它。
** 红色通道属性 **
对红色通道值进行硬编码并不是很好,所以我们想控制 QML 中的值。为此,我们在我们的着色器效果中添加一个 redChannel 属性,并在我们的片段着色器中声明一个 uniform lowp float redChannel。这就是为了从 QML 中可用的着色器代码创建一个值。很简单。
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;
}"
}
为了使镜头真正成为镜头,我们将 vec4 颜色改为 vec4(redChannel, 1.0, 1.0, 1.0),使颜色的其他部分乘以 1.0,只有红色部分乘以我们的 redChannel 变量。
** 红色通道动画 **
由于 redChannel 属性只是一个普通属性,它也可以像 QML 中的所有属性那样应用动画效果 。所以我们可以使用 QML 属性动画来改变属性的值,从而影响我们的 GPU 上的着色器。很酷炫吧!
ShaderEffect {
id: effect4
width: 80; height: width
property variant source: sourceImage
property real redChannel: 0.3
visible: root.step>3
NumberAnimation on redChannel {
from: 0.0; to: 1.0; loops: Animation.Infinite; duration: 4000
}
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;
}"
}
这里是最后的结果。
第二行的图片的着色器效果从 0.0 到 1.0,持续时间为 4 秒。因此,图像从没有红色信息(0.0 红色)到正常图像(1.0 红色)。
9.4 波形效果
在这个更复杂的例子中,我们将使用片段着色器创建一个波形效果。波形基于sin曲线,并且它影响了使用的纹理坐标的颜色。
import QtQuick 2.5
Rectangle {
width: 480; height: 240
color: '#1e1e1e'
Row {
anchors.centerIn: parent
spacing: 20
Image {
id: sourceImage
width: 160; height: width
source: "assets/coastline.jpg"
}
ShaderEffect {
width: 160; height: width
property variant source: sourceImage
property real frequency: 8
property real amplitude: 0.1
property real time: 0.0
NumberAnimation on time {
from: 0; to: Math.PI*2; duration: 1000; loops: Animation.Infinite
}
fragmentShader: "
varying highp vec2 qt_TexCoord0;
uniform sampler2D source;
uniform lowp float qt_Opacity;
uniform highp float frequency;
uniform highp float amplitude;
uniform highp float time;
void main() {
highp vec2 pulse = sin(time - frequency * qt_TexCoord0);
highp vec2 coord = qt_TexCoord0 + amplitude * vec2(pulse.x, -pulse.x);
gl_FragColor = texture2D(source, coord) * qt_Opacity;
}"
}
}
}
波的计算基于脉冲和纹理坐标操纵。我们使用一个基于当前时间与使用的纹理坐标的 sin 波浪方程式来实现脉冲:
highp vec2 pulse = sin(time - frequency * qt_TexCoord0);
如果没有时间因素,我们就会只有一个扭曲的图像,而不是像波浪那样移动的波纹效果。
对于颜色,我们使用不同纹理坐标的颜色:
highp vec2 coord = qt_TexCoord0 + amplitude * vec2(pulse.x, -pulse.x);
纹理坐标受到脉冲 x 值的影响。其结果是一个波浪的移动效果。
另外,如果我们还没有在这个片段着色器中移动像素,那么效果将首先像顶点着色器的作业一样。