I try to show a simple texture through opengl and Qt6 using custom shader classes.
- The image is a
QImage
encoded withFormat_RGBA8888
. I want this image to be shown on a customQQuickItem
. - This custom class uses a custom
QSGNode
class inside theQQuickItem::updatePaintNode
function. - The custom
QSGNode
uses customQSGMaterial
andQSGMaterialShader
classes too. - The
QSGMaterialShader
has 2 shader files, one for the fragment shader and the other for the vertex one.
For the example I created a QTimer which updates the source image every 5 seconds.
What I expect when I launch the application is to see the image on the UI.
But a fully black image was shown even if I wait for the image to be updated through the timer event.
First, to generate the image, I added a function inside my MainWindow
class:
This function is called by the timer each time it reaches its timeout. The returned image is finally stored inside the custom QQuickItem
instance.
MainWindow.cpp
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
ui->setupUi(this);
// register the custom C++ QQuickItem to be used by the QML
qmlRegisterType<OP_ImageViewer>("OP", 1, 0, "OP_ImageViewer"); // register the custom image viewer
// set source to 'main.qml'
// ...
QTimer* t = new QTimer(); // simulate the image change
t->setInterval(5000);
t->callOnTimeout([this]{
auto i = updateImage();
quickWidget->rootObject()->setProperty("sourceImage", QVariant::fromValue(i));
});
t->start();
}
QImage MainWindow::updateImage()
{
QSize sourceSize(512, 512);
QImage sourceImage(sourceSize, QImage::Format_RGBA8888);
// Loop through each pixel in the image
for (int y = 0; y < sourceSize.height(); ++y) {
for (int x = 0; x < sourceSize.width(); ++x) {
// Calculate the normalized gradient value
double gradient = (static_cast<double>(x) + y) / (sourceSize.width() + sourceSize.height() - 2);
// Scale the gradient to the maximum grayscale value (255 for Format_RGBA8888)
int grayValue = static_cast<int>(gradient * 255);
// Set the pixel value
if(count%2)
sourceImage.setPixelColor(x, y, QColor(255-grayValue, 0, grayValue));
else
sourceImage.setPixelColor(x, y, QColor(grayValue, 0, 255-grayValue));
}
}
count++; // Update the counter for image content update on timer event
return sourceImage;
}
The QMl file is made to show the custom QQuickItem
to the user.
main.qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import OP 1.0
Rectangle {
width: 800
height: 600
color: "transparent"
property var sourceImage
OP_ImageViewer {
anchors.fill: parent
anchors.margins: 5
image: sourceImage
}
The custom QQuickItem
is updated each time the source image is set. So the updatePaintNode
is called automatically.
Inside this function, I use my custom QSGGeometryNode
using the *oldNode
argument.
OP_ImageViewer.h (custom QQuickItem
)
class OP_ImageViewer : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QImage image WRITE setImage)
public:
OP_ImageViewer(QQuickItem *parent = nullptr)
{
setFlag(ItemHasContents, true);
}
void setImage(const QImage &image) {
if (image.isNull())
return;// a null image doesn't update the item
m_sourceImage = image;
update();
}
protected:
QImage m_sourceImage; // the image used for the texture
QSGNode *OP_ImageViewer::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override
{
ColormapNode* node = nullptr;
if(oldNode == nullptr)
node = new ColormapNode();
else
node = static_cast<ColormapNode*>(oldNode);
// update geometry
// ...
// update the source image
node->setTexture(window()->createTextureFromImage(m_sourceImage, QQuickWindow::TextureIsOpaque));
return node;
}
}
The setTexture
function is a custom function which will delete
the old texture (if one already exist) and set the given QSGTexture
, inside the custom QSGMaterial
instance
ColormapNode class inherited from QSGGeometryNode
class ColormapNode : public QSGGeometryNode
{
public:
// The constructor instances itself the needed objects
// The material, and the geometry
// Some Class content
void setTexture(QSGTexture* texture, bool updateUI = true)
{
if(updateTexture(m_texture, texture)) // the function replace the m_texture instance with the texture one, if both textures are equal, the function returns false, otherwise return true
m_material->state.texture = texture;
if(updateUI)
markDirty(QSGNode::DirtyMaterial);
}
private:
ColormapMaterial* m_material = nullptr;
QSGTexture* m_texture = nullptr;
}
The m_material is of type ColormapMaterial
which is my custom QSGMaterial
class.
ColormapMaterial class inherited from QSGMaterial
class ColormapMaterial : public QSGMaterial
{
public:
ColormapMaterial() { }
QSGMaterialType *type() const override; // instances a unique QSGMaterualType instance
int compare(const QSGMaterial *other) const override; // custom compare
QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override
{
return new ColormapShader; // return a custom shader instance
}
struct {
// stuff...
QSGTexture *texture = nullptr;
} state;
};
So this class allows me to instanciate a custom QSGMaterialShader
class to update the sampled image using the QSGMaterialShader::updateSampledImage
override.
ColormapShader class inherited from QSGMaterialShader
class ColormapShader : public QSGMaterialShader
{
public:
ColormapShader()
{
setShaderFileName(FragmentStage, QLatin1String(":/Shaders/Sources/imgh_colormap.frag.qsb"));
// set the vertex file too
}
void updateSampledImage(RenderState &state, int binding, QSGTexture **texture,
QSGMaterial *newMaterial, QSGMaterial *) override
{
Q_UNUSED(state);
ColormapMaterial *mat = static_cast<ColormapMaterial *>(newMaterial);
switch (binding) { // the binding for the sampler2Ds in the fragment shader
case 1:
*texture = mat->state.texture;
break;
// Other cases
default:
return;
}
}
Finally, there is the fragment shader file where the texture is processed by opengl.
imgh_colormap.frag
#version 440
layout(location = 0)in vec2 vTexCoord;
layout(location = 0) out vec4 fragColor;
layout(binding = 1) uniform sampler2D opTexture;
void main()
{
fragColor = texture(opTexture, vTexCoord);
}
Adding to those information, I made a standalone application on git if you want to test it easily
https://gitlab.com/adebono/qt6-porting-stackoverflow-example.
screenshots
I did those screenshots to image the problem. You can easily reproduce them using my git project
What I see using my custom Shader
What I should see
My hypothesis is:
I think the issue I encounter is during the QImage
conversion to QSGTexture
from the OP_ImageViewer
class as the QSGTexture
instance seems to be empty. To verify it I tried to retrieve the current OpenGL context inside the updatePaintNode
function but the instance is always nullptr
. As said inside the Qt’s doc:
Returns the last context which called makeCurrent in the current thread, or nullptr, if no context is current.
So did I missed to call the makeCurrent(...)
function somewhere ?
Thank you in advance for your answers.
adebono is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.