Vertex Shader

The vertex shader can be used to manipulate the vertexes provided by the shader effect. In normal cases, the shader effect has 4 vertexes (top-left, top-right, bottom-left and bottom-right). Each vertex reported is from type vec4. To visualize the vertex shader we will program a genie effect. This effect is used to let a rectangular window area vanish into one point, like a genie disappearing into a lamp.

image

Setting up the scene

First, we will set up our scene with an image and a shader effect.

  1. import QtQuick
  2. Rectangle {
  3. width: 480; height: 240
  4. color: '#1e1e1e'
  5. Image {
  6. id: sourceImage
  7. width: 160; height: width
  8. source: "../../assets/lighthouse.jpg"
  9. visible: false
  10. }
  11. Rectangle {
  12. width: 160; height: width
  13. anchors.centerIn: parent
  14. color: '#333333'
  15. }
  16. ShaderEffect {
  17. id: genieEffect
  18. width: 160; height: width
  19. anchors.centerIn: parent
  20. property variant source: sourceImage
  21. property bool minimized: false
  22. MouseArea {
  23. anchors.fill: parent
  24. onClicked: genieEffect.minimized = !genieEffect.minimized
  25. }
  26. }
  27. }

This provides a scene with a dark background and a shader effect using an image as the source texture. The original image is not visible on the image produced by our genie effect. Additional we added a dark rectangle on the same geometry as the shader effect so we can better detect where we need to click to revert the effect.

image

The effect is triggered by clicking on the image, this is defined by the mouse area covering the effect. In the onClicked handler we toggle the custom boolean property minimized. We will use this property later to toggle the effect.

Minimize and normalize

After we have set up the scene, we define a property of type real called minimize, the property will contain the current value of our minimization. The value will vary from 0.0 to 1.0 and is controlled by a sequential animation.

  1. property real minimize: 0.0
  2. SequentialAnimation on minimize {
  3. id: animMinimize
  4. running: genieEffect.minimized
  5. PauseAnimation { duration: 300 }
  6. NumberAnimation { to: 1; duration: 700; easing.type: Easing.InOutSine }
  7. PauseAnimation { duration: 1000 }
  8. }
  9. SequentialAnimation on minimize {
  10. id: animNormalize
  11. running: !genieEffect.minimized
  12. NumberAnimation { to: 0; duration: 700; easing.type: Easing.InOutSine }
  13. PauseAnimation { duration: 1300 }
  14. }

The animation is triggered by the toggling of the minimized property. Now that we have set up all our surroundings we finally can look at our vertex shader.

  1. #version 440
  2. layout(location=0) in vec4 qt_Vertex;
  3. layout(location=1) in vec2 qt_MultiTexCoord0;
  4. layout(location=0) out vec2 qt_TexCoord0;
  5. layout(std140, binding=0) uniform buf {
  6. mat4 qt_Matrix;
  7. float qt_Opacity;
  8. float minimize;
  9. float width;
  10. float height;
  11. } ubuf;
  12. out gl_PerVertex {
  13. vec4 gl_Position;
  14. };
  15. void main() {
  16. qt_TexCoord0 = qt_MultiTexCoord0;
  17. vec4 pos = qt_Vertex;
  18. pos.y = mix(qt_Vertex.y, ubuf.height, ubuf.minimize);
  19. pos.x = mix(qt_Vertex.x, ubuf.width, ubuf.minimize);
  20. gl_Position = ubuf.qt_Matrix * pos;
  21. }

The vertex shader is called for each vertex so four times, in our case. The default qt defined parameters are provided, like qt_Matrix, qt_Vertex, qt_MultiTexCoord0, qt_TexCoord0. We have discussed the variable already earlier. Additional we link the minimize, width and height variables from our shader effect into our vertex shader code. In the main function, we store the current texture coordinate in our qt_TexCoord0 to make it available to the fragment shader. Now we copy the current position and modify the x and y position of the vertex:

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

The mix(…) function provides a linear interpolation between the first 2 parameters on the point (0.0-1.0) provided by the 3rd parameter. So in our case, we interpolate for y between the current y position and the hight based on the current minimized value, similar for x. Bear in mind the minimized value is animated by our sequential animation and travels from 0.0 to 1.0 (or vice versa).

image

The resulting effect is not really the genie effect but is already a great step towards it.

Primitive Bending

So minimized the x and y components of our vertexes. Now we would like to slightly modify the x manipulation and make it depending on the current y value. The needed changes are pretty small. The y-position is calculated as before. The interpolation of the x-position depends now on the vertexes y-position:

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

This results in an x-position tending towards the width when the y-position is larger. In other words, the upper 2 vertexes are not affected at all as they have a y-position of 0 and the lower two vertexes x-positions both bend towards the width, so they bend towards the same x-position.

image

Better Bending

As the bending is not really satisfying currently we will add several parts to improve the situation. First, we enhance our animation to support an own bending property. This is necessary as the bending should happen immediately and the y-minimization should be delayed shortly. Both animations have in the sum the same duration (300+700+1000 and 700+1300).

We first add and animate bend from QML.

  1. property real bend: 0.0
  2. property bool minimized: false
  3. // change to parallel animation
  4. ParallelAnimation {
  5. id: animMinimize
  6. running: genieEffect.minimized
  7. SequentialAnimation {
  8. PauseAnimation { duration: 300 }
  9. NumberAnimation {
  10. target: genieEffect; property: 'minimize';
  11. to: 1; duration: 700;
  12. easing.type: Easing.InOutSine
  13. }
  14. PauseAnimation { duration: 1000 }
  15. }
  16. // adding bend animation
  17. SequentialAnimation {
  18. NumberAnimation {
  19. target: genieEffect; property: 'bend'
  20. to: 1; duration: 700;
  21. easing.type: Easing.InOutSine }
  22. PauseAnimation { duration: 1300 }
  23. }
  24. }

We then add bend to the uniform buffer, ubuf and use it in the shader to achive a smoother bending.

  1. #version 440
  2. layout(location=0) in vec4 qt_Vertex;
  3. layout(location=1) in vec2 qt_MultiTexCoord0;
  4. layout(location=0) out vec2 qt_TexCoord0;
  5. layout(std140, binding=0) uniform buf {
  6. mat4 qt_Matrix;
  7. float qt_Opacity;
  8. float minimize;
  9. float width;
  10. float height;
  11. float bend;
  12. } ubuf;
  13. out gl_PerVertex {
  14. vec4 gl_Position;
  15. };
  16. void main() {
  17. qt_TexCoord0 = qt_MultiTexCoord0;
  18. vec4 pos = qt_Vertex;
  19. pos.y = mix(qt_Vertex.y, ubuf.height, ubuf.minimize);
  20. float t = pos.y / ubuf.height;
  21. t = (3.0 - 2.0 * t) * t * t;
  22. pos.x = mix(qt_Vertex.x, ubuf.width, t * ubuf.bend);
  23. gl_Position = ubuf.qt_Matrix * pos;
  24. }

The curve starts smooth at the 0.0 value, grows then and stops smoothly towards the 1.0 value. Here is a plot of the function in the specified range. For us, only the range from 0..1 is from interest.

image

We also need to increase the number of vertex points. The vertex points used can be increased by using a mesh.

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

The shader effect now has an equality distributed grid of 16x16 vertexes instead of the 2x2 vertexes used before. This makes the interpolation between the vertexes look much smoother.

image

You can see also the influence of the curve being used, as the bending smoothes at the end nicely. This is where the bending has the strongest effect.

Choosing Sides

As a final enhancement, we want to be able to switch sides. The side is towards which point the genie effect vanishes. Until now it vanishes always towards the width. By adding a side property we are able to modify the point between 0 and width.

  1. ShaderEffect {
  2. ...
  3. property real side: 0.5
  4. ...
  5. }
  1. #version 440
  2. layout(location=0) in vec4 qt_Vertex;
  3. layout(location=1) in vec2 qt_MultiTexCoord0;
  4. layout(location=0) out vec2 qt_TexCoord0;
  5. layout(std140, binding=0) uniform buf {
  6. mat4 qt_Matrix;
  7. float qt_Opacity;
  8. float minimize;
  9. float width;
  10. float height;
  11. float bend;
  12. float side;
  13. } ubuf;
  14. out gl_PerVertex {
  15. vec4 gl_Position;
  16. };
  17. void main() {
  18. qt_TexCoord0 = qt_MultiTexCoord0;
  19. vec4 pos = qt_Vertex;
  20. pos.y = mix(qt_Vertex.y, ubuf.height, ubuf.minimize);
  21. float t = pos.y / ubuf.height;
  22. t = (3.0 - 2.0 * t) * t * t;
  23. pos.x = mix(qt_Vertex.x, ubuf.side * ubuf.width, t * ubuf.bend);
  24. gl_Position = ubuf.qt_Matrix * pos;
  25. }

image

Packaging

The last thing to-do is packaging our effect nicely. For this, we extract our genie effect code into an own component called GenieEffect. It has the shader effect as the root element. We removed the mouse area as this should not be inside the component as the triggering of the effect can be toggled by the minimized property.

  1. // GenieEffect.qml
  2. import QtQuick
  3. ShaderEffect {
  4. id: genieEffect
  5. width: 160; height: width
  6. anchors.centerIn: parent
  7. property variant source
  8. mesh: GridMesh { resolution: Qt.size(10, 10) }
  9. property real minimize: 0.0
  10. property real bend: 0.0
  11. property bool minimized: false
  12. property real side: 1.0
  13. ParallelAnimation {
  14. id: animMinimize
  15. running: genieEffect.minimized
  16. SequentialAnimation {
  17. PauseAnimation { duration: 300 }
  18. NumberAnimation {
  19. target: genieEffect; property: 'minimize';
  20. to: 1; duration: 700;
  21. easing.type: Easing.InOutSine
  22. }
  23. PauseAnimation { duration: 1000 }
  24. }
  25. SequentialAnimation {
  26. NumberAnimation {
  27. target: genieEffect; property: 'bend'
  28. to: 1; duration: 700;
  29. easing.type: Easing.InOutSine }
  30. PauseAnimation { duration: 1300 }
  31. }
  32. }
  33. ParallelAnimation {
  34. id: animNormalize
  35. running: !genieEffect.minimized
  36. SequentialAnimation {
  37. NumberAnimation {
  38. target: genieEffect; property: 'minimize';
  39. to: 0; duration: 700;
  40. easing.type: Easing.InOutSine
  41. }
  42. PauseAnimation { duration: 1300 }
  43. }
  44. SequentialAnimation {
  45. PauseAnimation { duration: 300 }
  46. NumberAnimation {
  47. target: genieEffect; property: 'bend'
  48. to: 0; duration: 700;
  49. easing.type: Easing.InOutSine }
  50. PauseAnimation { duration: 1000 }
  51. }
  52. }
  53. vertexShader: "genieeffect.vert.qsb"
  54. }
  1. // genieeffect.vert
  2. #version 440
  3. layout(location=0) in vec4 qt_Vertex;
  4. layout(location=1) in vec2 qt_MultiTexCoord0;
  5. layout(location=0) out vec2 qt_TexCoord0;
  6. layout(std140, binding=0) uniform buf {
  7. mat4 qt_Matrix;
  8. float qt_Opacity;
  9. float minimize;
  10. float width;
  11. float height;
  12. float bend;
  13. float side;
  14. } ubuf;
  15. out gl_PerVertex {
  16. vec4 gl_Position;
  17. };
  18. void main() {
  19. qt_TexCoord0 = qt_MultiTexCoord0;
  20. vec4 pos = qt_Vertex;
  21. pos.y = mix(qt_Vertex.y, ubuf.height, ubuf.minimize);
  22. float t = pos.y / ubuf.height;
  23. t = (3.0 - 2.0 * t) * t * t;
  24. pos.x = mix(qt_Vertex.x, ubuf.side * ubuf.width, t * ubuf.bend);
  25. gl_Position = ubuf.qt_Matrix * pos;
  26. }

You can use now the effect simply like this:

  1. import QtQuick
  2. Rectangle {
  3. width: 480; height: 240
  4. color: '#1e1e1e'
  5. GenieEffect {
  6. source: Image { source: '../../assets/lighthouse.jpg' }
  7. MouseArea {
  8. anchors.fill: parent
  9. onClicked: parent.minimized = !parent.minimized
  10. }
  11. }
  12. }

We have simplified the code by removing our background rectangle and we assigned the image directly to the effect, instead of loading it inside a standalone image element.