# 顶点着色器

顶点着色器可用于操纵着色器效果提供的顶点。 在正常情况下,着色器效果有4个顶点(左上、右上、左下和右下),呈现的每个顶点都使用vec4类型。 为了将顶点着色器可视化,将编写一个精灵特效。 此特效用于让矩形窗口区域变成为一个点后消失,就像精灵消失在灯中一样。

image

# 设置场景

首先,使用一幅图像和一个着色器效果设置场景。

import QtQuick

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

这里提供了一个场景,这个场景带有深色的背景和一个图像作为源纹理的着色器效果。原始的图像在使用精灵效果后产生的图像上不可见。此外,在与着色器效果外形相同的位置上添加了一个深色矩形,它用来可以更好的检测需要单击来恢复效果的区域位置。

image

通过单击图像来触发效果,这是通过覆盖效果的鼠标区域定义的。 在onClicked处理器中,切换自定义的布尔属性minimized。 稍后将使用此属性来切换效果。

# 最小化和标准化

在设置好场景后,定义了一个名为minimize的real类型的属性,该属性将包含最小化程度的当前值。 该值将在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属性的切换触发。 现在已经设置好了所有的环境,终于可以观察顶点着色器了。

#version 440

layout(location=0) in vec4 qt_Vertex;
layout(location=1) in vec2 qt_MultiTexCoord0;

layout(location=0) out vec2 qt_TexCoord0;

layout(std140, binding=0) uniform buf {
    mat4 qt_Matrix;
    float qt_Opacity;

    float minimize;
    float width;
    float height;
} ubuf;

out gl_PerVertex { 
    vec4 gl_Position;
};

void main() {
    qt_TexCoord0 = qt_MultiTexCoord0;
    vec4 pos = qt_Vertex;
    pos.y = mix(qt_Vertex.y, ubuf.height, ubuf.minimize);
    pos.x = mix(qt_Vertex.x, ubuf.width, ubuf.minimize);
    gl_Position = ubuf.qt_Matrix * pos;
}

在例子中,每个顶点调用顶点着色器四次。 提供了默认的qt定义参数,如qt_Matrixqt_Vertexqt_MultiTexCoord0qt_TexCoord0,这些是之前已经讨论过的变量。 此外,将着色器效果中的minimizewidthheight变量链接到顶点着色器代码中。 在main函数中,将当前纹理坐标存储在qt_TexCoord0 中,以使其可用于片段着色器。 现在复制当前位置并修改顶点的x和y位置:

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

mix(…)提供了在前两个参数之间进行线性插值的函数,第3个参数提供(0.0-1.0)之间的插值比例的点。 因此,在此例子中,根据当前的最小值(ubuf.minimize)在当前y位置和高度之间对y进行插值;对于x也类似这样。 请记住,最小化的值由顺序动画进行动画处理,并从0.0变动到1.0;反之亦然。

image

由此产生的效果并不是真正的精灵效果,但已经朝着它迈出了一大步。

# 原始的弯曲

那么,最小化了顶点的x和y分量。 现在,想稍微修改x的操作并使其取决于当前的y值。 所需的更改非常小,y位置如前所述一样计算,x位置的插值现在取决于顶点y的位置:

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

当y位置较大时,会导致x的位置趋于宽度(译者注释:ubuf.height是一个定值,pos.y越大,t则越大;在下面的式子中,t * minimize越大,根据线性插值的原理,则计算出来的pos.x越接近ubuf.width)。 换句话说,上面的2个顶点根本不受影响,因为它们的y位置为0(译者注释:pos.y等于0的时候,t也为0,那么在第二个式子中,t * minimize为0,则pos.x的值为qt_Vertex.x,所以最上面的两个顶点不受影响。);而下面的两个顶点的x位置都向宽度弯曲,因此它们向相同的x位置弯曲(译者注释:在最下面的点,存在pos.y = ubuf.height,=> t = 1 => t * minimize = minimize)。

image

# 更好的弯曲

由于弯曲效果并不是真正令人满意,目前将增加几个部分内容,以改善效果。
首先,增强动画以支持的弯曲特性。这是必要的,因为弯曲应立即发生,y值最小化应有短暂延迟。两个动画的总持续时间相同(300+700+1000和700+1300)。

首先从QML上添加bend并设置其动画。

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

然后,将bend添加到统一缓冲区ubuf,并在着色器中使用它来实现更平滑的弯曲。

#version 440

layout(location=0) in vec4 qt_Vertex;
layout(location=1) in vec2 qt_MultiTexCoord0;

layout(location=0) out vec2 qt_TexCoord0;

layout(std140, binding=0) uniform buf {
    mat4 qt_Matrix;
    float qt_Opacity;

    float minimize;
    float width;
    float height;
    float bend;
} ubuf;

out gl_PerVertex { 
    vec4 gl_Position;
};

void main() {
    qt_TexCoord0 = qt_MultiTexCoord0;
    vec4 pos = qt_Vertex;
    pos.y = mix(qt_Vertex.y, ubuf.height, ubuf.minimize);
    float t = pos.y / ubuf.height;
    t = (3.0 - 2.0 * t) * t * t;
    pos.x = mix(qt_Vertex.x, ubuf.width, t * ubuf.bend);
    gl_Position = ubuf.qt_Matrix * pos;
}

曲线在0.0值处开始平滑增长,然后增长到1.0值处停止。下面是是指定范围内函数的曲线图。对于此示例而言,只关注0到1范围的情况。

image

还需要增加顶点的数量,使用网格可以增加使用的顶点。

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

着色器效果现在具有由16x16个顶点组成的均匀分布的网格,而不是以前使用的2x2个顶点。这使得顶点之间的插值看起来更加平滑。

image

可以看到所使用的曲线的影响,弯曲会在末端平滑。这是弯曲效果最强的地方。

# 选择边缘

最后,继续增强效果,希望能够改变精灵效果时两侧的边缘。 边缘朝着精灵效应消失的地方。 直到现在,它总是朝着宽度方向(译者注释:最下面的最右点,也就是在最下面线的长度方向)消失。 通过添加side属性,可以将消失位置修改为0到宽度之间的点。

ShaderEffect {
    ...
    property real side: 0.5
    ...
}
#version 440

layout(location=0) in vec4 qt_Vertex;
layout(location=1) in vec2 qt_MultiTexCoord0;

layout(location=0) out vec2 qt_TexCoord0;

layout(std140, binding=0) uniform buf {
    mat4 qt_Matrix;
    float qt_Opacity;

    float minimize;
    float width;
    float height;
    float bend;
    float side;
} ubuf;

out gl_PerVertex { 
    vec4 gl_Position;
};

void main() {
    qt_TexCoord0 = qt_MultiTexCoord0;
    vec4 pos = qt_Vertex;
    pos.y = mix(qt_Vertex.y, ubuf.height, ubuf.minimize);
    float t = pos.y / ubuf.height;
    t = (3.0 - 2.0 * t) * t * t;
    pos.x = mix(qt_Vertex.x, ubuf.side * ubuf.width, t * ubuf.bend);
    gl_Position = ubuf.qt_Matrix * pos;
}

image

# Packaging

最后要做的就是很好地打包效果。 为此,将精灵效果代码提取到一个名为GenieEffect的自定义组件中。 它将着色器效果作为根元素。 移除了鼠标区域,它不应该在组件内部,其效果的触发可以通过minimized属性进行切换。

// GenieEffect.qml
import QtQuick

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: "genieeffect.vert.qsb"
}
// genieeffect.vert
#version 440

layout(location=0) in vec4 qt_Vertex;
layout(location=1) in vec2 qt_MultiTexCoord0;

layout(location=0) out vec2 qt_TexCoord0;

layout(std140, binding=0) uniform buf {
    mat4 qt_Matrix;
    float qt_Opacity;

    float minimize;
    float width;
    float height;
    float bend;
    float side;
} ubuf;

out gl_PerVertex { 
    vec4 gl_Position;
};

void main() {
    qt_TexCoord0 = qt_MultiTexCoord0;
    vec4 pos = qt_Vertex;
    pos.y = mix(qt_Vertex.y, ubuf.height, ubuf.minimize);
    float t = pos.y / ubuf.height;
    t = (3.0 - 2.0 * t) * t * t;
    pos.x = mix(qt_Vertex.x, ubuf.side * ubuf.width, t * ubuf.bend);
    gl_Position = ubuf.qt_Matrix * pos;
}

现在可以像下面这样简单地使用效果:

import QtQuick

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

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

通过删除背景矩形来简化代码,并将图像直接分配给效果,而不是将其加载到独立的图像元素中。

最后更新: 1/15/2022, 11:25:48 PM