basysKom Application Development Services

Qt 6: How To port Shader Effects from Qt 5
Essential Summary
In this blog post, we give you a step-by-step guide on how to port shader code you might have in your Qt 5 application to Qt 6.

Introduction

Recently, we wrote about porting a Qt 5 application to Qt 6. The bigger part of the work is due to changes in Qt’s graphics logic – especially when you have lots of shaders strewn across your QML code.

In this blog post, we introduce a step-by-step guide on how to move the shader code you might have in your Qt 5 application to Qt 6.

Quick Overview

To sum it up, it mostly comes down to the following changes:

  • Move embedded shader strings from QML to separate files
  • Add a version to the first line in your shader files
  • The attribute and varying qualifiers got removed, meaning you have to rewrite your variable declarations. All variable declarations should get a layout qualifier. This process is described in detail below
  • Remove the precision specifiers (highp and lowp)
  • Fragment shader’s gl_position output becomes an ordinary out variable
  • Port the deprecated glsl functions to their new counterparts, i.e. change occurrences of texture2D to texture

Porting the shaders

We have prepared a minimal example to demonstrate the porting process. It has a vertex shader deforming the incoming vertices to generate a wiggling effect and a fragment shader to blend between two abstract images back and forth. Below is the whole main.qml:

import QtQuick 2.15
import QtQuick.Window 2.12
 
Window {
    id: window
 
    width: melting.implicitWidth
    height: melting.implicitHeight
    visible: true
    color: "black"
 
    ShaderEffect {
        id: wiggleEffect
 
        property real strength: 5.0
        property real time: 50.0
        property real blendingProgress: -1
        property variant source1: firstImageSource
        property variant source2: secondImageSource
 
        anchors.centerIn: parent
        width: triangle.width
        height: triangle.height
 
        mesh: GridMesh {
            resolution: Qt.size(20, 20)
        }
 
        UniformAnimator on time {
            from: 0
            to:  100
            duration: 2000
            loops: -1
            running: true
        }
 
        UniformAnimator on blendingProgress {
            from: -1
            to:  1
            duration: 10000
            loops: -1
            running: true
        }
 
        vertexShader: "
                float noise(vec2 uv){
                    return fract(sin(dot(uv, vec2(12.9898,78.233))) * 43758.5453123);
                }
 
                uniform highp mat4 qt_Matrix;
                uniform lowp float strength;
                uniform lowp float time;
                attribute highp vec4 qt_Vertex;
                attribute highp vec2 qt_MultiTexCoord0;
                varying highp vec2 qt_TexCoord0;
                void main() {
                    qt_TexCoord0 = qt_MultiTexCoord0;
                    highp vec4 pos = qt_Vertex;
                    lowp float angle = 2. * 3.141 * (noise(pos.xy) + time / 100.0);
                    lowp float strengthWithVariation = strength * noise(pos.yx);
                    pos.x += cos(angle) * strengthWithVariation;
                    pos.y += sin(angle) * strengthWithVariation;
                    gl_Position = qt_Matrix * pos;
                }"
 
        fragmentShader: "
                varying highp vec2 qt_TexCoord0;
                uniform lowp float qt_Opacity;
                uniform lowp float blendingProgress;
                uniform lowp sampler2D source1;
                uniform lowp sampler2D source2;
                void main() {
                    lowp float alpha = abs(blendingProgress);
                    gl_FragColor = ((1. - alpha) * texture2D(source1, qt_TexCoord0)
                                   + alpha * texture2D(source2, qt_TexCoord0))
                                   * qt_Opacity;
                }"
 
    }
 
    ShaderEffectSource {
        id: firstImageSource
 
        live: false
        hideSource: true
        sourceItem: Image {
              id: melting
 
              width: implicitWidth
              height: implicitHeight
              source: "qrc:/melting.jpg"
              fillMode: Image.PreserveAspectFit
        }
    }
 
    ShaderEffectSource {
        id: triangle
 
        live: false
        hideSource: true
        sourceItem: Image {
              id: triangle
 
              width: implicitWidth
              height: implicitHeight
              source: "qrc:/triangle.jpg"
              fillMode: Image.PreserveAspectFit
        }
    }
} 
As you can see, there is one ShaderEffect component coming with both, a vertex shader and a fragment shader. The shader code is embedded as strings, as it’s often the case in Qt 5 applications.

To port the ShaderEffect, we first have to outsource the shader strings. Create a directory with the name shader and add two files to the directory:
  • wiggleblender.vert
  • wiggleblender.frag

(or whatever names you give to your shaders; use appropriate file extensions to distinguish between fragment and vertex shaders).

Copy the shader strings into the corresponding files. Now change the ShaderEffect properties fragmentShader and vertexShader in your main.qml accordingly:
ShaderEffect {
    id: wiggleEffect
 
    // ...
 
    vertexShader: "qrc:/shader/wiggleblender.vert.qsb"
    fragmentShader: "qrc:/shader/wiggleblender.frag.qsb"
 
} 
Note that the file extension we are using here is qsb – which stands for Qt Shader Baker. Qsb files can contain different shader variants – one for each of the supported graphics backends like OpenGL, Vulkan, DirectX or Metal. We are going to create qsb files later on from the ported shader files.

You can reference the qsb files directly, but in production code it’s often better to access assets via Qt´s resource system.

Porting the shader code is straightforward.
#version 440                                                                       // 1
 
layout(location = 0) in vec4 position;                                             // 2
layout(location = 1) in vec2 texcoord;
 
layout(location = 0) out vec2 coord;                                               // 3
 
layout(std140, binding = 0) uniform buf {                                          // 4
    mat4 qt_Matrix;                                                                // 5
    float qt_Opacity;
 
    float strength;
    float time;
    float blendingProgress;
} ubuf;
 
out gl_PerVertex { vec4 gl_Position; };                                            //6
 
float noise(vec2 uv){
    return fract(sin(dot(uv, vec2(12.9898,78.233))) * 43758.5453123);
}
 
void main() {
    coord = texcoord;
    vec4 pos = position;
    float angle = 2. * 3.141 * (noise(pos.xy) + ubuf.time / 100.0);                // 7
    float strengthWithVariation = ubuf.strength * noise(pos.yx);
    pos.x += cos(angle) * strengthWithVariation;
    pos.y += sin(angle) * strengthWithVariation;
    gl_Position = ubuf.qt_Matrix * pos;
} 
Below is a step-by-step recipe you can use for your own code:
  1. Add a version string
  2. Add these two lines to fetch the vertex data. Qt fills a vertex buffer with vertex positions at location 0 and texture coordinates at location 1. Instead of using the attribute qualifier as before, now we declare inputs to the vertex shader by writing modern glsls code using layout and in qualifiers
  3. Declare the outputs you would like to pass to the fragment shader – these are the varying declarations in your non-ported code. For each output variable you have to add a line in the form layout (location = n) out type name and increment n starting at 0.
  4. Declare the uniforms. They are passed directly from QML properties to the shader code by Qt. All uniforms except samplers are provided via a uniform buffer. Qt always binds the buffer at 0 and stores the variables qt_Matrix and qt_Opacity at the very beginning. Note that you have to add std1440, which determines how the uniform buffer is laid out in memory
  5. Remove all highp and lowp specifiers
  6. Optional; the vertex shader writes its output to a built-in variable. If you leave this line out, the graphics engine still knows what to do
  7. Don’t forget to access the uniform variables via the uniform buffer (easy to miss)

Porting the fragment shader works in a similar fashion:
#version 440                                                 # 1
 
layout(location = 0) in vec2 texCoord;                       # 2
layout(location = 0) out vec4 fragColor;                     # 3
 
layout(std140, binding = 0) uniform buf {                    # 4
    mat4 qt_Matrix;
    float qt_Opacity;
 
    float strength;
    float time;
    float blendingProgress;
} ubuf;
 
layout(binding = 1) uniform sampler2D source1;              # 5
layout(binding = 2) uniform sampler2D source2;
 
void main() {
    float alpha = abs(ubuf.blendingProgress);
    fragColor = ((1. - alpha) * texture(source1, texCoord)  # 6, 7
                   + alpha * texture(source2, texCoord))
                   * ubuf.qt_Opacity;                       # 8
} 
  1. Add a version string
  2. Declare the variables coming from the vertex shader, i.e. the varying declarations in the non-ported code. You can just copy all variable declarations with out qualifiers from the vertex shader and replace all out keywords with in. If you don’t have a vertex shader, just copy the line as given in the code snippet above – Qt passes the texture coordinate from the default vertex shader. Note that the layout locations need to match (this does not apply to the variable names, which may differ).
  3. Declare the output – a vec4 color. Fragment shader output must be declared explicitly. Output of the color to gl_FragColor does not work anymore.
  4. Add the uniform buffer (the same that we added to the vertex shader)
  5. Declare all samplers. Don’t forget to assign different bindings. Note that the binding point 0 is reserved for the uniform buffer by Qt.
  6. Port gl_FragColor to whatever name you have used for the out variable
  7. Change all occurrences of texture2D to texture
  8. Again, access uniforms via the uniform buffer

Baking the shaders

If everything has been ported correctly, you are finally able to bake the shader files. There are two ways to achieve this:
  • Manually
  • Let the build system handle the details
Of course, delegating the shader baking to your build system is recommended, but in the early Qt6 days things can still be a bit flaky. We explain both cases – manually baking the shaders into qsb files and using CMake as the build system

Manually

Use the command line tool qsb as described below:
qsb --glsl 100es,120,150 --hlsl 50 --msl 12 -b -o  wiggleblender.vert.qsb wiggleblender.vert
qsb --glsl 100es,120,150 --hlsl 50 --msl 12    -o  wiggleblender.frag.qsb wiggleblender.frag 
This creates the vertex and fragment shader qsb files suitable for rendering with OpenGL, Vulkan, DirectX and Metal backends. The -b parameter adds some magic so that the scenegraph renderer is able to properly batch the ShaderEffect. For more information on qsb and its parameters, please consult the official documentation. Optionally add the files to the Qt resource system:
<RCC>
    <qresource prefix="/">
        <file>qml/Shader/qsb/wiggleblender.vert.qsb</file>
        <file>qml/Shader/qsb/wiggleblender.frag.qsb</file>
    </qresource>
</RCC> 

Using CMake

Add the following lines to CMakeLists.txt:

qt6_add_shaders(${PROJECT_NAME} "porting-example-shaders"
    BATCHABLE
    PRECOMPILE
    OPTIMIZED
    PREFIX
        "/shader"
    FILES
        "shader/wiggleblender.vert"
        "shader/wiggleblender.frag"
) 

Unfortunately, as of today there seems to be a bug when the Makefile generator is used, meaning the qsb command line tool is not called. In the future qsb should just be an arbitrary build step like calling moc on header files, working silently in the background.

Conclusion

Qt made the transition from Qt5 to Qt6 a breeze. What we have learned is, that if you port from Qt5 to Qt6 and have refined your QML code with a lot of shaders, you need to pay attention on this detail. While following our guide gives you the right tools at hand to master this challenge, feel free to drop a comment down below if any question arise while doing so.

Picture of Berthold Krevert

Berthold Krevert

Since over 10 years, Berthold Krevert works as a senior software developer here at basysKom GmbH. He has a strong background in developing user interfaces using technologies like Qt, QtQuick and HTML5 for embedded systems and industrial applications. He holds a diploma of computer science from the University of Paderborn. During his studies he focused - amongst other topics - on medical imaging systems and machine learning. His interests include data visualisation, image processing and modern graphic APIs and how to utilize them to render complex UIs in 2d and 3d.

7 Responses

  1. This is helpful, but for a complete noob like me, it’s apparently not enough.

    I end up with the following error when loading my shaders from Qt:

    Failed to link shader program: error: uniform `ubuf’ declared as type `buf’ and type `buf’

    The sentence doesn’t even end. What does that even mean ? I am completely lost.

    And what has become of qt_MultiTexCoord0 ?
    Also, we can’t use booleans anymore ? (tells me that unsigned int aren’t allowed in legacy targets when I try to use bool)

    • Hi Michael,

      1)
      regarding your first question:

      It appears that you have declared different uniform blocks in the vertex and fragment shaders using the same type name:

      For example:

      Vertex shader:
      layout(std140, binding = 0) uniform SomeBuffer {
      vec2 resolution;
      int flip;
      } ubuf;

      Fragment shader:
      layout(std140, binding = 0) uniform SomeBuffer{
      vec2 resolution;
      } ubuf;

      In the vertex shader, the SomeBuffer type includes both a resolution and a flip variable, whereas in the fragment shader, it only contains a resolution variable. To resolve this, one should add the flip parameter to the SomeBuffer declaration in the fragment shader as well.

      2)
      You can refer to this example to see how to use qt_MultiTexCoord0:
      https://github.com/qt/qtdeclarative/blob/f956eada99ea9926a917f24a1afb03382406d346/tests/baseline/scenegraph/data/shared/shaders/wave.vert#L4

      3)
      The GLSL code is cross-compiled by Qt Shader Tools. It is transpiled into SPIR-V as an intermediate format and then further translated for different targets. If one of the targets doesn’t support boolean (unsigned integer) variables, the SPIR-V compiler prints this error message (as far as I know.) It’s worth noting that you can substitute boolean variables with integers (similar to the uniform buffer example provided above).

  2. Thank you so much for this. It honestly helped me a lot.
    Most resources cover the use of shaders with Qt5 and even this blog post was a bit buried in the google search results. It’s not easy to find good material for Qt6.

  3. Thanks very much for those useful information.
    I still have an issue that I don’t understand: either compile it manually or via CMake, it fails and I have the following log message:

    QSpirvCompiler: Link failed
    Shader baking failed:

    Do you have an idea what cause this issue and where can I look to solve it?

    • This error provides very little context.

      Can you check your qt_add_shaders() invokation? Can you pass in DEBUGINFO and make sure that QUITE isn’t present?

      • Sorry I didn’t know what to put for additional context.
        Actually my issue was that I was using “mainImage” instead of “main”.
        I thought shaders were compatible with shadertoy, but apparently it’s not the case.
        Thanks for your quick answer!

Leave a Reply

Your email address will not be published. Required fields are marked *

More Blogarticles

basysKom Newsletter

We collect only the data you enter in this form (no IP address or information that can be derived from it). The collected data is only used in order to send you our regular newsletters, from which you can unsubscribe at any point using the link at the bottom of each newsletter. We will retain this information until you ask us to delete it permanently. For more information about our privacy policy, read Privacy Policy