Qt 6.6.1 Android. glReadPixels reads the pixel color from wrong position

Hello,

I read the pixel color and print it to the console. It looks like triangle is not on his place:

pick-triangle-opengl-qt6

main.cpp

#include <QtGui/QOpenGLFunctions>
#include <QtGui/QMouseEvent>
#include <QtOpenGL/QOpenGLBuffer>
#include <QtOpenGL/QOpenGLShaderProgram>
#include <QtOpenGLWidgets/QOpenGLWidget>
#include <QtWidgets/QApplication>
#include <QtWidgets/QLabel>
#include <QtWidgets/QVBoxLayout>
#include <QtWidgets/QWidget>

class OpenGLWidget : public QOpenGLWidget, private QOpenGLFunctions
{
    Q_OBJECT

private:
    void initializeGL() override
    {
        initializeOpenGLFunctions();
        glClearColor(0.f, 1.f, 0.f, 1.f);

        QString vertexShaderSource =
            "attribute vec2 aPosition;\n"
            "void main()\n"
            "{\n"
            "    gl_Position = vec4(aPosition, 0.0, 1.0);\n"
            "}\n";

        QString fragmentShaderSource =
            "#ifdef GL_ES\n"
            "precision mediump float;\n"
            "#endif\n"
            "//out vec4 fragColor;\n"
            "void main()\n"
            "{\n"
            "    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n"
            "}\n";

        m_program.addShaderFromSourceCode(QOpenGLShader::ShaderTypeBit::Vertex,
                                          vertexShaderSource);
        m_program.addShaderFromSourceCode(QOpenGLShader::ShaderTypeBit::Fragment,
                                          fragmentShaderSource);
        m_program.link();
        m_program.bind();

        float vertPositions[] = {
            -0.5f, -0.5f,
            0.5f, -0.5f,
            0.f, 0.5f
        };
        m_vertPosBuffer.create();
        m_vertPosBuffer.bind();
        m_vertPosBuffer.allocate(vertPositions, sizeof(vertPositions));
        m_aPositionLocation = m_program.attributeLocation("aPosition");
    }

    void paintGL() override
    {
        glClear(GL_COLOR_BUFFER_BIT);

        m_program.setAttributeBuffer(m_aPositionLocation, GL_FLOAT, 0, 2);
        m_program.enableAttributeArray(m_aPositionLocation);
        glDrawArrays(GL_TRIANGLES, 0, 3);

        if (m_mouseClicked)
        {
            // Read the pixel
            GLubyte pixel[4];
            glReadPixels(m_mouseX, m_mouseY, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
            qDebug() << pixel[0] / 255.f << pixel[1] / 255.f << pixel[2] / 255.f;
            m_mouseClicked = false;
        }
    }

    void mousePressEvent(QMouseEvent *event) override
    {
        m_mouseX = event->pos().x();
        m_mouseY = height() - event->pos().y() - 1;
        m_mouseClicked = true;
        update();
    }

private:
    int m_mouseX;
    int m_mouseY;
    bool m_mouseClicked = false;
    QOpenGLBuffer m_vertPosBuffer;
    QOpenGLShaderProgram m_program;
    int m_aPositionLocation;
};

class MainWindow : public QWidget
{
    Q_OBJECT

public:
    MainWindow()
    {
        setWindowTitle("OpenGL, Qt6, C++");
        resize(300, 300);

        m_nameLabel = new QLabel("Click on object or background");
        m_nameLabel->setSizePolicy(QSizePolicy::Policy::Fixed, QSizePolicy::Policy::Fixed);
        OpenGLWidget *openGLWidget = new OpenGLWidget();

        QVBoxLayout *layout = new QVBoxLayout();
        layout->addWidget(m_nameLabel);
        layout->addWidget(openGLWidget);
        setLayout(layout);
    }

private:
    QLabel *m_nameLabel;
};

#include "main.moc"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    MainWindow w;
    w.show();
    return app.exec();
}

pro

QT += core gui openglwidgets widgets

win32: LIBS += -lopengl32

CONFIG += c++17

SOURCES += \
    main.cpp

TARGET = app

Cross-refs:

I tried to call glGetError() after glReadPixels() but it returns 0.

        glReadPixels(m_mouseX, m_mouseY, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
        qDebug() << glGetError();

I created a bug report: glReadPixels reads the pixel color from wrong position on Android You can download a zip of example from the bug report.

I rewrote the example to JavaScript/WebGL and built it to APK with using Cordova. It works without problems with glReadPixels. But EXE with Cordova requires 150 MB. I don’t like it. I think it is good idea to use Qt for Desktop/WebAssembly and WebGL/Cordova for Android and Web.

> npm install cordova -g
> npm install http-server -g
> cordova create read-pixels-webgl-js io.github.ivan_8observer8.read_pixels AppClass
> emulator.exe -avd GalaxyNexus
> cordova build android
> cd platforms\android\app\build\outputs\apk\debug
> adb install app-debug.apk
> adb logcat "eglCodecCommon:S"

If you changed a source code you should to rebuild an app for Android: cordova build android, uninstall the app: adb uninstall io.github.ivan_8observer8.read_pixels and install it again: adb install app-debug.apk

read-pixels-webgl-js

Demo: read-pixels-webgl-js - Replit

index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, width=device-width, viewport-fit=cover">
    <link rel="stylesheet" type="text/css" href="css/style.css">
    <title>Read Pixels</title>
</head>

<body>
    <canvas id="renderCanvas" width="350" height="350"></canvas>

    <script type="module" src="./js/index.js"></script>
</body>

</html>

js/index.js

import { gl, initWebGLContext } from "./webgl-context.js";

function init() {
    console.log("------------------init------------------");
    if (!initWebGLContext("renderCanvas")) return;

    gl.clearColor(0.2, 0.2, 0.2, 1.0);

    const vertexShaderSource =
        `
            attribute vec2 aPosition;
            
            void main()
            {
                gl_Position = vec4(aPosition, 0.0, 1.0);
            }`;

    const fragmentShaderSource =
        `
            precision mediump float;
            
            void main()
            {
                gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
            }`;

    const vShader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vShader, vertexShaderSource);
    gl.compileShader(vShader);

    const fShader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(fShader, fragmentShaderSource);
    gl.compileShader(fShader);

    const program = gl.createProgram();
    gl.attachShader(program, vShader);
    gl.attachShader(program, fShader);
    gl.linkProgram(program);
    gl.useProgram(program);

    const vertices = new Float32Array([
        0, 0.5,
        -0.5, -0.5,
        0.5, -0.5
    ]);

    const vbo = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

    const aPositionLocation = gl.getAttribLocation(program, "aPosition");
    gl.vertexAttribPointer(aPositionLocation, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(aPositionLocation);

    window.onresize = () => {
        const w = gl.canvas.clientWidth;
        const h = gl.canvas.clientHeight;
        gl.canvas.width = w;
        gl.canvas.height = h;
        gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
        draw();
    };
    window.onresize(null);

    gl.canvas.onclick = (event) => {
        const rect = event.target.getBoundingClientRect();
        const x = event.clientX - rect.left;
        const y = rect.bottom - event.clientY - 1;

        draw();

        const pixels = new Uint8Array(4);
        gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
        const output = `${(pixels[0] / 255.0).toFixed(2)} ${(pixels[1] / 255.0).toFixed(2)} ${(pixels[2] / 255.0).toFixed(2)} `;
        // console.log(
        //     (pixels[0] / 255.0).toFixed(2),
        //     (pixels[1] / 255.0).toFixed(2),
        //     (pixels[2] / 255.0).toFixed(2));
        console.log(output);
    };
}

function draw() {
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES, 0, 3);
}

init();

js/webgl-context.js

export let gl = null;

export function initWebGLContext(canvasName) {
    const canvas = document.getElementById(canvasName);
    if (canvas === null) {
        console.log(`Failed to get a canvas element with the name "${canvasName}"`);
        return false;
    }
    gl = canvas.getContext("webgl", { alpha: false, premultipliedAlpha: false });
    return true;
}

css/style.css

html,
body {
    overflow: hidden;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
}

#renderCanvas {
    width: 100%;
    height: 100%;
}

I found a solution in comments here: OpenGL support broken with high-dpi (Retina) on OS X. It is for macOS but it is true for Android too. The comment of Laszlo Agocs helped me:

You need to adjust the GL positions based on the devicePixelRatio(). If the window is size N,M and devicePixelRatio() is 2 then the GL framebuffer, viewport will all have a size of 2N,2M. Try multiplying mouseX and mouseY with devicePixelRatio().

Solution:

    void mousePressEvent(QMouseEvent *event) override
    {
        m_mouseX = event->pos().x() * devicePixelRatio();
        m_mouseY = (height() - event->pos().y() - 1) * devicePixelRatio();
        m_mouseClicked = true;
        update();
    }

pick-color-of-simple-triangle-qopenglwindow-qt6-cpp-android-emulator

main.cpp

#include <QtGui/QMouseEvent>
#include <QtGui/QOpenGLFunctions>
#include <QtOpenGL/QOpenGLBuffer>
#include <QtOpenGL/QOpenGLShaderProgram>
#include <QtOpenGL/QOpenGLWindow>
#include <QtWidgets/QApplication>

class OpenGLWindow : public QOpenGLWindow, private QOpenGLFunctions
{
    int m_mouseX;
    int m_mouseY;
    bool m_mouseClicked = false;
    QOpenGLBuffer m_vertPosBuffer;
    QOpenGLShaderProgram m_program;

    void initializeGL() override
    {
        initializeOpenGLFunctions();
        glClearColor(0.2f, 0.2f, 0.2f, 1.f);
        qDebug() << "Device pixel ratio:" << devicePixelRatio();

        QString vertexShaderSource =
            "attribute vec2 aPosition;\n"
            "void main()\n"
            "{\n"
            "    gl_Position = vec4(aPosition, 0.0, 1.0);\n"
            "}\n";

        QString fragmentShaderSource =
            "#ifdef GL_ES\n"
            "precision mediump float;\n"
            "#endif\n"
            "void main()\n"
            "{\n"
            "    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n"
            "}\n";

        m_program.create();
        m_program.addShaderFromSourceCode(QOpenGLShader::ShaderTypeBit::Vertex,
                                          vertexShaderSource);
        m_program.addShaderFromSourceCode(QOpenGLShader::ShaderTypeBit::Fragment,
                                          fragmentShaderSource);
        m_program.link();
        m_program.bind();

        float vertPositions[] = {
            -0.5f, -0.5f,
            0.5f, -0.5f,
            0.f, 0.5f
        };
        m_vertPosBuffer.create();
        m_vertPosBuffer.bind();
        m_vertPosBuffer.allocate(vertPositions, sizeof(vertPositions));

        m_program.setAttributeBuffer("aPosition", GL_FLOAT, 0, 2);
        m_program.enableAttributeArray("aPosition");
    }

    void paintGL() override
    {
        glClear(GL_COLOR_BUFFER_BIT);
        glDrawArrays(GL_TRIANGLES, 0, 3);

        if (m_mouseClicked)
        {
            // Read the pixel
            GLubyte pixel[4];
            glReadPixels(m_mouseX, m_mouseY, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
            // qDebug() << glGetError() << "\n";
            qDebug() << pixel[0] / 255.f << pixel[1] / 255.f << pixel[2] / 255.f;
            m_mouseClicked = false;
        }
    }

    void mousePressEvent(QMouseEvent *event) override
    {
        m_mouseX = event->pos().x() * devicePixelRatio();
        m_mouseY = (height() - event->pos().y() - 1) * devicePixelRatio();
        m_mouseClicked = true;
        update();
    }
};

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    OpenGLWindow w;
    w.show();
    return app.exec();
}

pick-color-of-simple-triangle-qopenglwindow-qt6-cpp.pro

QT += core gui opengl widgets

win32: LIBS += -lopengl32

CONFIG += c++17

SOURCES += \
    main.cpp

TARGET = app