中文版《Qt5 Cadaques》

頂點著色器(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
        }
    }
}

我們簡化了代碼,移除了背景矩形框,直接使用圖片完成效果,替換了在一個單獨的圖像元素中加載它。