着色器效果(Shader Effect)

注意

最后一次构建:2014年1月20日下午18:00。

这章的源代码能够在assetts folder找到。

着色器允许我们利用SceneGraph的接口直接调用在强大的GPU上运行的OpenGL来创建渲染效果。着色器使用ShaderEffect与ShaderEffectSource元素来实现。着色器本身的算法使用OpenGL Shading Language(OpenGL着色语言)来实现。

实际上这意味着你需要混合使用QML代码与着色器代码。执行时,会将着色器代码发送到GPU,并在GPU上编译执行。QML着色器元素(Shader QML Elements)允许你与OpenGL着色器程序的属性交互。

让我们首先来看看OpenGL着色器。

OpenGL着色器(OpenGL Shader)

OpenGL的渲染管线分为几个步骤。一个简单的OpenGL渲染管线将包含一个顶点着色器和一个片段着色器。

顶点着色器接收顶点数据,并且在程序最后赋值给gl_Position。然后,顶点将会被裁剪,转换和栅格化后作为像素输出。
片段(像素)进入片段着色器,进一步对片段操作并将结果的颜色赋值给gl_FragColor。顶点着色器调用多边形每个角的点(顶点=3D中的点),负责这些点的3D处理。片段(片度=像素)着色器调用每个像素并决定这个像素的颜色。

着色器元素(Shader Elements)

为了对着色器编程,Qt Quick提供了两个元素。ShaderEffectSource与ShaderEffect。ShaderEffect将会使用自定义的着色器,ShaderEffectSource可以将一个QML元素渲染为一个纹理然后再渲染这个纹理。由于ShaderEffect能够应用自定义的着色器到它的矩形几何形状,并且能够使用在着色器中操作资源。一个资源可以是一个图片,它被作为一个纹理或者着色器资源。

默认下着色器使用这个资源并且不作任何改变进行渲染。

import QtQuick 2.0

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张图片,第一张是原始图片,第二张使用默认的着色器渲染出来的图片,第三张使用了Qt5源码中默认的顶点与片段着色器的代码进行渲染的图片。

注意

如果你不想看到原始图片,而只想看到被着色器渲染后的图片,你可以设置Image为不可见(visible:false)。着色器仍然会使用图片数据,但是图像元素(Image Element)将不会被渲染。

让我们仔细看看着色器代码。

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浮点数(float)矩阵

  • vec2-包含两个浮点数的向量

  • sampler2D-2D纹理

  • float-浮点数

可以查看OpenGL ES 2.0 API Quick Reference Card获得更多信息。

现在我们可以更好的理解下面这些变量:

  • 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)的像素颜色与透明值相乘。

fragmentShader: "
    varying highp vec2 qt_TexCoord0;
    uniform sampler2D source;
    uniform lowp float qt_Opacity;
    void main() {
        gl_FragColor = texture2D(source, qt_TexCoord0) * qt_Opacity;
    }"

在后面的例子中,我们将会展示一些简单的着色器例子。首先我们会集中在片段着色器上,然后在回到顶点着色器上。

片段着色器(Fragement Shader)

片段着色器调用每个需要渲染的像素。我们将开发一个红色透镜,它将会增加图片的红色通道的值。

配置场景(Setting up the scene)

首先我们配置我们的场景,在区域中央使用一个网格显示我们的源图片(source image)。

import QtQuick 2.0

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'
        }
    }
}

红色着色器(A red Shader)

下一步我们添加一个着色器,显示一个红色矩形框。由于我们不需要纹理,我们从顶点着色器中移除纹理。

vertexShader: "
    uniform highp mat4 qt_Matrix;
    attribute highp vec4 qt_Vertex;
    void main() {
        gl_Position = qt_Matrix * qt_Vertex;
    }"
fragmentShader: "
    uniform lowp float qt_Opacity;
    void main() {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0) * qt_Opacity;
    }"

在片段着色器中,我们简单的给gl_FragColor赋值为vec4(1.0, 0.0, 0.0, 1.0),它代表红色, 并且不透明(alpha=1.0)。

使用纹理的红色着色器(A red shader with texture)

现在我们想要将这个红色应用在纹理的每个像素上。我们需要将纹理加回顶点着色器。由于我们不再在顶点着色器中做任何其它的事情,所以默认的顶点着色器已经满足我们的要求。

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;
        }"
}

完整的着色器重新包含我们的源图片作为属性,由于我们没有特殊指定,使用默认的顶点着色器,我没有重写顶点着色器。

在片段着色器中,我们提取纹理片段texture2D(source,qt_TexCoord0),并且与红色一起应用。

红色通道属性(The red channel property)

这样的代码用来修改红色通道的值看起来不是很好,所以我们想要将这个值包含在QML这边。我们在ShaderEffect中增加一个redChannel属性,并在我们的片段着色器中申明一个uniform lowpfloat 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变量。

红色通道的动画(The red channel animated

由于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;
        }"
}

下面是最后的结果。

在这4秒内,第二排的着色器红色通道的值从0.0到1.0。图片从没有红色信息(0.0 red)到一个正常的图片(1.0 red)。

波浪效果(Wave Effect)

在这个更加复杂的例子中,我们使用片段着色器创建一个波浪效果。波浪的形成是基于sin曲线,并且它影响了使用的纹理坐标的颜色。

import QtQuick 2.0

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脉冲值影响,结果就像一个移动的波浪。

如果我们没有在片段着色器中使用像素的移动,这个效果可以首先考虑使用顶点着色器来完成。

顶点着色器(Vertex Shader)

顶点着色器用来操作ShaderEffect提供的顶点。正常情况下,ShaderEffect有4个顶点(左上top-left,右上top-right,左下bottom-left,右下bottom-right)。每个顶点使用vec4类型记录。为了实现顶点着色器的可视化,我们将编写一个吸收的效果。这个效果通常被用来让一个矩形窗口消失为一个点。

配置场景(Setting up the scene)

首先我们再一次配置场景。

import QtQuick 2.0

Rectangle {
    width: 480; height: 240
    color: '#1e1e1e'

    Image {
        id: sourceImage
        width: 160; height: width
        source: "assets/lighthouse.jpg"
        visible: false
    }
    Rectangle {
        width: 160; height: width
        anchors.centerIn: parent
        color: '#333333'
    }
    ShaderEffect {
        id: genieEffect
        width: 160; height: width
        anchors.centerIn: parent
        property variant source: sourceImage
        property bool minimized: false
        MouseArea {
            anchors.fill: parent
            onClicked: genieEffect.minimized = !genieEffect.minimized
        }
    }
}

这个场景使用了一个黑色背景,并且提供了一个使用图片作为资源纹理的ShaderEffect。使用image元素的原图片是不可见的,只是给我们的吸收效果提供资源。此外我们在ShaderEffect的位置添加了一个同样大小的黑色矩形框,这样我们可以更加明确的知道我们需要点击哪里来重置效果。

点击图片将会触发效果,MouseArea覆盖了ShaderEffect。在onClicked操作中,我们绑定了自定义的布尔变量属性minimized。我们稍后使用这个属性来触发效果。

最小化与正常化(Minimize and normalize)

在我们配置好场景后,我们定义一个real类型的属性,叫做minimize,这个属性包含了我们当前最小化的值。这个值在0.0到1.0之间,由一个连续的动画来控制它。

property real minimize: 0.0

SequentialAnimation on minimize {
    id: animMinimize
    running: genieEffect.minimized
    PauseAnimation { duration: 300 }
    NumberAnimation { to: 1; duration: 700; easing.type: Easing.InOutSine }
    PauseAnimation { duration: 1000 }
}

SequentialAnimation on minimize {
    id: animNormalize
    running: !genieEffect.minimized
    NumberAnimation { to: 0; duration: 700; easing.type: Easing.InOutSine }
    PauseAnimation { duration: 1300 }
}

这个动画绑定了由minimized属性触发。现在我们已经配置好我们的环境,最后让我们看看顶点着色器的代码。

vertexShader: "
    uniform highp mat4 qt_Matrix;
    attribute highp vec4 qt_Vertex;
    attribute highp vec2 qt_MultiTexCoord0;
    varying highp vec2 qt_TexCoord0;
    uniform highp float minimize;
    uniform highp float width;
    uniform highp float height;
    void main() {
        qt_TexCoord0 = qt_MultiTexCoord0;
        highp vec4 pos = qt_Vertex;
        pos.y = mix(qt_Vertex.y, height, minimize);
        pos.x = mix(qt_Vertex.x, width, minimize);
        gl_Position = qt_Matrix * pos;
    }"

顶点着色器被每个顶点调用,在我们这个例子中,一共调用了四次。默认下提供qt已定义的参数,如qt_Matrix,qt_Vertex,qt_MultiTexCoord0,qt_TexCoord0。我们在之前已经讨论过这些变量。此外我们从ShaderEffect中链接minimize,width与height的值到我们的顶点着色器代码中。在main函数中,我们将当前纹理值保存在qt_TexCoord()中,让它在片段着色器中可用。现在我们拷贝当前位置,并修改顶点的x,y的位置。

highp vec4 pos = qt_Vertex;
pos.y = mix(qt_Vertex.y, height, minimize);
pos.x = mix(qt_Vertex.x, width, minimize);

mix(…)函数提供了一种在两个参数之间(0.0到1.0)的线性插值的算法。在我们的例子中,在当前y值与高度值之间基于minimize的值插值获得y值,x的值获取类似。记住minimize的值是由我们的连续动画控制,并且在0.0到1.0之间(反之亦然)。

这个结果的效果不是真正吸收效果,但是已经能朝着这个目标完成了一大步。

基础弯曲(Primitive Bending)

我们已经完成了最小化我们的坐标。现在我们想要修改一下对x值的操作,让它依赖当前的y值。这个改变很简单。y值计算在前。x值的插值基于当前顶点的y坐标。

highp float t = pos.y / height;
pos.x = mix(qt_Vertex.x, width, t * minimize);

这个结果造成当y值比较大时,x的位置更靠近width的值。也就是说上面2个顶点根本不受影响,它们的y值始终为0,下面两个顶点的x坐标值更靠近width的值,它们最后转向同一个x值。

import QtQuick 2.0

Rectangle {
    width: 480; height: 240
    color: '#1e1e1e'

    Image {
        id: sourceImage
        width: 160; height: width
        source: "assets/lighthouse.jpg"
        visible: false
    }
    Rectangle {
        width: 160; height: width
        anchors.centerIn: parent
        color: '#333333'
    }
    ShaderEffect {
        id: genieEffect
        width: 160; height: width
        anchors.centerIn: parent
        property variant source: sourceImage
        property real minimize: 0.0
        property bool minimized: false


        SequentialAnimation on minimize {
            id: animMinimize
            running: genieEffect.minimized
            PauseAnimation { duration: 300 }
            NumberAnimation { to: 1; duration: 700; easing.type: Easing.InOutSine }
            PauseAnimation { duration: 1000 }
        }

        SequentialAnimation on minimize {
            id: animNormalize
            running: !genieEffect.minimized
            NumberAnimation { to: 0; duration: 700; easing.type: Easing.InOutSine }
            PauseAnimation { duration: 1300 }
        }


        vertexShader: "
            uniform highp mat4 qt_Matrix;
            uniform highp float minimize;
            uniform highp float height;
            uniform highp float width;
            attribute highp vec4 qt_Vertex;
            attribute highp vec2 qt_MultiTexCoord0;
            varying highp vec2 qt_TexCoord0;
            void main() {
                qt_TexCoord0 = qt_MultiTexCoord0;
                // M1>>
                highp vec4 pos = qt_Vertex;
                pos.y = mix(qt_Vertex.y, height, minimize);
                highp float t = pos.y / height;
                pos.x = mix(qt_Vertex.x, width, t * minimize);
                gl_Position = qt_Matrix * pos;

更好的弯曲(Better Bending)

现在简单的弯曲并不能真正的满足我们的要求,我们将添加几个部件来提升它的效果。首先我们增加动画,支持一个自定义的弯曲属性。这是非常必要的,由于弯曲立即发生,y值的最小化需要被推迟。两个动画在同一持续时间计算总和(300+700+100与700+1300)。

property real bend: 0.0
property bool minimized: false


// change to parallel animation
ParallelAnimation {
    id: animMinimize
    running: genieEffect.minimized
    SequentialAnimation {
        PauseAnimation { duration: 300 }
        NumberAnimation {
            target: genieEffect; property: 'minimize';
            to: 1; duration: 700;
            easing.type: Easing.InOutSine
        }
        PauseAnimation { duration: 1000 }
    }
    // adding bend animation
    SequentialAnimation {
        NumberAnimation {
            target: genieEffect; property: 'bend'
            to: 1; duration: 700;
            easing.type: Easing.InOutSine }
        PauseAnimation { duration: 1300 }
    }
}

此外,为了使弯曲更加平滑,不再使用y值影响x值的弯曲函数,pos.x现在依赖新的弯曲属性动画:

highp float t = pos.y / height;
t = (3.0 - 2.0 * t) * t * t;
pos.x = mix(qt_Vertex.x, width, t * bend);

弯曲从0.0平滑开始,逐渐加快,在1.0时逐渐平滑。下面是这个函数在指定范围内的曲线图。对于我们,只需要关注0到1的区间。

想要获得最大化的视觉改变,需要增加我们的顶点数量。可以使用网眼(mesh)来增加顶点:

mesh: GridMesh { resolution: Qt.size(16, 16) }

现在ShaderEffect被分布为16x16顶点的网格,替换了之前2x2的顶点。这样顶点之间的插值将会看起来更加平滑。

你可以看见曲线的变化,在最后让弯曲变得非常平滑。这让弯曲有了更加强大的效果。

侧面收缩(Choosing Sides)

最后一个增强,我们希望能够收缩边界。边界朝着吸收的点消失。直到现在它总是在朝着width值的点消失。添加一个边界属性,我们能够修改这个点在0到width之间。

ShaderEffect {
    ...
    property real side: 0.5

    vertexShader: "
        ...
        uniform highp float side;
        ...
        pos.x = mix(qt_Vertex.x, side * width, t * bend);
    "
}

包装(Packing)

最后将我们的效果包装起来。将我们吸收效果的代码提取到一个叫做GenieEffect的自定义组件中。它使用ShaderEffect作为根元素。移除掉MouseArea,这不应该放在组件中。绑定minimized属性来触发效果。

import QtQuick 2.0

ShaderEffect {
    id: genieEffect
    width: 160; height: width
    anchors.centerIn: parent
    property variant source
    mesh: GridMesh { resolution: Qt.size(10, 10) }
    property real minimize: 0.0
    property real bend: 0.0
    property bool minimized: false
    property real side: 1.0


    ParallelAnimation {
        id: animMinimize
        running: genieEffect.minimized
        SequentialAnimation {
            PauseAnimation { duration: 300 }
            NumberAnimation {
                target: genieEffect; property: 'minimize';
                to: 1; duration: 700;
                easing.type: Easing.InOutSine
            }
            PauseAnimation { duration: 1000 }
        }
        SequentialAnimation {
            NumberAnimation {
                target: genieEffect; property: 'bend'
                to: 1; duration: 700;
                easing.type: Easing.InOutSine }
            PauseAnimation { duration: 1300 }
        }
    }

    ParallelAnimation {
        id: animNormalize
        running: !genieEffect.minimized
        SequentialAnimation {
            NumberAnimation {
                target: genieEffect; property: 'minimize';
                to: 0; duration: 700;
                easing.type: Easing.InOutSine
            }
            PauseAnimation { duration: 1300 }
        }
        SequentialAnimation {
            PauseAnimation { duration: 300 }
            NumberAnimation {
                target: genieEffect; property: 'bend'
                to: 0; duration: 700;
                easing.type: Easing.InOutSine }
            PauseAnimation { duration: 1000 }
        }
    }

    vertexShader: "
        uniform highp mat4 qt_Matrix;
        attribute highp vec4 qt_Vertex;
        attribute highp vec2 qt_MultiTexCoord0;
        uniform highp float height;
        uniform highp float width;
        uniform highp float minimize;
        uniform highp float bend;
        uniform highp float side;
        varying highp vec2 qt_TexCoord0;
        void main() {
            qt_TexCoord0 = qt_MultiTexCoord0;
            highp vec4 pos = qt_Vertex;
            pos.y = mix(qt_Vertex.y, height, minimize);
            highp float t = pos.y / height;
            t = (3.0 - 2.0 * t) * t * t;
            pos.x = mix(qt_Vertex.x, side * width, t * bend);
            gl_Position = qt_Matrix * pos;
        }"
}

你现在可以像这样简单的使用这个效果:

import QtQuick 2.0

Rectangle {
    width: 480; height: 240
    color: '#1e1e1e'

    GenieEffect {
        source: Image { source: 'assets/lighthouse.jpg' }
        MouseArea {
            anchors.fill: parent
            onClicked: parent.minimized = !parent.minimized
        }
    }
}

我们简化了代码,移除了背景矩形框,直接使用图片完成效果,替换了在一个单独的图像元素中加载它。

剧幕效果(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。

import QtQuick 2.0

ShaderEffect {
    anchors.fill: parent

    mesh: GridMesh {
        resolution: Qt.size(50, 50)
    }

    property real topWidth: open?width:20
    property real bottomWidth: topWidth
    property real amplitude: 0.1
    property bool open: false
    property variant source: effectSource

    Behavior on bottomWidth {
        SpringAnimation {
            easing.type: Easing.OutElastic;
            velocity: 250; mass: 1.5;
            spring: 0.5; damping: 0.05
        }
    }

    Behavior on topWidth {
        NumberAnimation { duration: 1000 }
    }


    ShaderEffectSource {
        id: effectSource
        sourceItem: effectImage;
        hideSource: true
    }

    Image {
        id: effectImage
        anchors.fill: parent
        source: "assets/fabric.jpg"
        fillMode: Image.Tile
    }

    vertexShader: "
        attribute highp vec4 qt_Vertex;
        attribute highp vec2 qt_MultiTexCoord0;
        uniform highp mat4 qt_Matrix;
        varying highp vec2 qt_TexCoord0;
        varying lowp float shade;

        uniform highp float topWidth;
        uniform highp float bottomWidth;
        uniform highp float width;
        uniform highp float height;
        uniform highp float amplitude;

        void main() {
            qt_TexCoord0 = qt_MultiTexCoord0;

            highp vec4 shift = vec4(0.0, 0.0, 0.0, 0.0);
            highp float swing = (topWidth - bottomWidth) * (qt_Vertex.y / height);
            shift.x = qt_Vertex.x * (width - topWidth + swing) / width;

            shade = sin(21.9911486 * qt_Vertex.x / width);
            shift.y = amplitude * (width - topWidth + swing) * shade;

            gl_Position = qt_Matrix * (qt_Vertex - shift);

            shade = 0.2 * (2.0 - shade ) * ((width - topWidth + swing) / width);
        }"

    fragmentShader: "
        uniform sampler2D source;
        varying highp vec2 qt_TexCoord0;
        varying lowp float shade;
        void main() {
            highp vec4 color = texture2D(source, qt_TexCoord0);
            color.rgb *= 1.0 - shade;
            gl_FragColor = color;
        }"
}

这个效果在curtaindemo.qml文件中使用。

import QtQuick 2.0

Rectangle {
    id: root
    width: 480; height: 240
    color: '#1e1e1e'

    Image {
        anchors.centerIn: parent
        source: 'assets/wiesn.jpg'
    }

    CurtainEffect {
        id: curtain
        anchors.fill: parent
    }

    MouseArea {
        anchors.fill: parent
        onClicked: curtain.open = !curtain.open
    }
}

剧幕效果通过自定义的open属性打开。我们使用了一个MouseArea来触发打开和关闭剧幕。

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) 使用一个阈值,一个源项遮挡另一个源项

下面是一个使用快速模糊效果的例子:

import QtQuick 2.0
import QtGraphicalEffects 1.0

Rectangle {
    width: 480; height: 240
    color: '#1e1e1e'

    Row {
        anchors.centerIn: parent
        spacing: 16

        Image {
            id: sourceImage
            source: "assets/tulips.jpg"
            width: 200; height: width
            sourceSize: Qt.size(parent.width, parent.height)
            smooth: true
        }

        FastBlur {
            width: 200; height: width
            source: sourceImage
            radius: blurred?32:0
            property bool blurred: false

            Behavior on radius {
                NumberAnimation { duration: 1000 }
            }

            MouseArea {
                id: area
                anchors.fill: parent
                onClicked: parent.blurred = !parent.blurred
            }
        }
    }
}

左边是原图片。点击右边的图片将会触发blurred属性,模糊在1秒内从0到32。左边显示模糊后的图片。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 2291184112@qq.com

×

喜欢就点赞,疼爱就打赏