Curtain Effect

In the last example for custom shader effects, I would like to bring you the curtain effect. This effect was published first in May 2011 as part of Qt labs for shader effectsCurtain Effect - 图1 (opens new window).

image

At that time I really loved these effects and the curtain effect was my favorite out of them. I just love how the curtain opens and hide the background object.

For this chapter, the effect has been adapted for Qt 6. It has also been slghtly simplifed to make it a better showcase.

The curtain image is called fabric.png. The effect then uses a vertex shader to swing the curtain forth and back and a fragment shader to apply shadows to show how the fabric folds.

The diagram below shows how the shader works. The waves are computed through a sin curve with 7 periods (7*PI=21.99…). The other part is the swinging. The topWidht of the curtain is animated when the curtain is opened or closed. The bottomWidth follows the topWidth using a SpringAnimation. This creates the effect of the bottom part of the curtain swinging freely. The calulated swing component is the strengh of the swing based on the y-component of the vertexes.

image

The curtain effect is implemed in the CurtainEffect.qml file where the fabric image act as the texture source. In the QML code, the mesh property is adjusted to make sure that the number of vertixes is increased to give a smoother result.

  1. import QtQuick
  2. ShaderEffect {
  3. anchors.fill: parent
  4. mesh: GridMesh {
  5. resolution: Qt.size(50, 50)
  6. }
  7. property real topWidth: open?width:20
  8. property real bottomWidth: topWidth
  9. property real amplitude: 0.1
  10. property bool open: false
  11. property variant source: effectSource
  12. Behavior on bottomWidth {
  13. SpringAnimation {
  14. easing.type: Easing.OutElastic;
  15. velocity: 250; mass: 1.5;
  16. spring: 0.5; damping: 0.05
  17. }
  18. }
  19. Behavior on topWidth {
  20. NumberAnimation { duration: 1000 }
  21. }
  22. ShaderEffectSource {
  23. id: effectSource
  24. sourceItem: effectImage;
  25. hideSource: true
  26. }
  27. Image {
  28. id: effectImage
  29. anchors.fill: parent
  30. source: "../assets/fabric.png"
  31. fillMode: Image.Tile
  32. }
  33. vertexShader: "curtain.vert.qsb"
  34. fragmentShader: "curtain.frag.qsb"
  35. }

The vertex shader, shown below, reshapes the curtain based on the topWidth and bottomWidth properies, extrapolating the shift based on the y-coordinate. It also calculates the shade value, which is used in the fragment shader. The shade property is passed through an additional output in location 1.

  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(location=1) out float shade;
  6. layout(std140, binding=0) uniform buf {
  7. mat4 qt_Matrix;
  8. float qt_Opacity;
  9. float topWidth;
  10. float bottomWidth;
  11. float width;
  12. float height;
  13. float amplitude;
  14. } ubuf;
  15. out gl_PerVertex {
  16. vec4 gl_Position;
  17. };
  18. void main() {
  19. qt_TexCoord0 = qt_MultiTexCoord0;
  20. vec4 shift = vec4(0.0, 0.0, 0.0, 0.0);
  21. float swing = (ubuf.topWidth - ubuf.bottomWidth) * (qt_Vertex.y / ubuf.height);
  22. shift.x = qt_Vertex.x * (ubuf.width - ubuf.topWidth + swing) / ubuf.width;
  23. shade = sin(21.9911486 * qt_Vertex.x / ubuf.width);
  24. shift.y = ubuf.amplitude * (ubuf.width - ubuf.topWidth + swing) * shade;
  25. gl_Position = ubuf.qt_Matrix * (qt_Vertex - shift);
  26. shade = 0.2 * (2.0 - shade) * ((ubuf.width - ubuf.topWidth + swing) / ubuf.width);
  27. }

In the fragment shader below, the shade is picked up as an input in location 1 and is then used to calculate the fragColor, which is used to draw the pixel in question.

  1. #version 440
  2. layout(location=0) in vec2 qt_TexCoord0;
  3. layout(location=1) in float shade;
  4. layout(location=0) out vec4 fragColor;
  5. layout(std140, binding=0) uniform buf {
  6. mat4 qt_Matrix;
  7. float qt_Opacity;
  8. float topWidth;
  9. float bottomWidth;
  10. float width;
  11. float height;
  12. float amplitude;
  13. } ubuf;
  14. layout(binding=1) uniform sampler2D source;
  15. void main() {
  16. highp vec4 color = texture(source, qt_TexCoord0);
  17. color.rgb *= 1.0 - shade;
  18. fragColor = color;
  19. }

The combination of QML animations and passing variables from the vertex shader to the fragment shader demonstrates how QML and shaders can be used together to build complex, animated, effects.

The effect itself is used from the curtaindemo.qml file shown below.

  1. import QtQuick
  2. Item {
  3. id: root
  4. width: background.width; height: background.height
  5. Image {
  6. id: background
  7. anchors.centerIn: parent
  8. source: '../assets/background.png'
  9. }
  10. Text {
  11. anchors.centerIn: parent
  12. font.pixelSize: 48
  13. color: '#efefef'
  14. text: 'Qt 6 Book'
  15. }
  16. CurtainEffect {
  17. id: curtain
  18. anchors.fill: parent
  19. }
  20. MouseArea {
  21. anchors.fill: parent
  22. onClicked: curtain.open = !curtain.open
  23. }
  24. }

The curtain is opened through a custom open property on the curtain effect. We use a MouseArea to trigger the opening and closing of the curtain when the user clicks or taps the area.