From 4d3d05acc7db1c431540e018c5cc5db5edd965ba Mon Sep 17 00:00:00 2001 From: Johannes Sauer Date: Thu, 1 Oct 2020 20:54:36 +0200 Subject: [PATCH 01/29] using the qt cube opengl example as a basis. modified the separate view to run the cube demo instead --- YUViewLib/YUViewLib.pro | 4 +- YUViewLib/images/cube.png | Bin 0 -> 30341 bytes YUViewLib/images/textures.qrc | 5 + YUViewLib/shaders/fshader.glsl | 18 ++ YUViewLib/shaders/shaders.qrc | 6 + YUViewLib/shaders/vshader.glsl | 24 ++ YUViewLib/src/ui/mainwindow.cpp | 12 +- YUViewLib/src/ui/separateWindow.cpp | 42 ++- YUViewLib/src/ui/separateWindow.h | 8 +- YUViewLib/src/ui/views/openGLViewWidget.cpp | 330 ++++++++++++++++++++ YUViewLib/src/ui/views/openGLViewWidget.h | 110 +++++++ 11 files changed, 543 insertions(+), 16 deletions(-) create mode 100644 YUViewLib/images/cube.png create mode 100644 YUViewLib/images/textures.qrc create mode 100644 YUViewLib/shaders/fshader.glsl create mode 100644 YUViewLib/shaders/shaders.qrc create mode 100644 YUViewLib/shaders/vshader.glsl create mode 100644 YUViewLib/src/ui/views/openGLViewWidget.cpp create mode 100644 YUViewLib/src/ui/views/openGLViewWidget.h diff --git a/YUViewLib/YUViewLib.pro b/YUViewLib/YUViewLib.pro index 2e6f1da52..8568817a4 100644 --- a/YUViewLib/YUViewLib.pro +++ b/YUViewLib/YUViewLib.pro @@ -14,7 +14,9 @@ INCLUDEPATH += src/ RESOURCES += \ images/images.qrc \ - docs/docs.qrc + docs/docs.qrc \ + images/textures.qrc \ + shaders/shaders.qrc contains(QT_ARCH, x86_32|i386) { warning("You are building for a 32 bit system. This is untested and not supported.") diff --git a/YUViewLib/images/cube.png b/YUViewLib/images/cube.png new file mode 100644 index 0000000000000000000000000000000000000000..42c8c51b3a004d3caaab6d9ab08f3f58d284c9ca GIT binary patch literal 30341 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 literal 0 HcmV?d00001 diff --git a/YUViewLib/images/textures.qrc b/YUViewLib/images/textures.qrc new file mode 100644 index 000000000..fe53be5e6 --- /dev/null +++ b/YUViewLib/images/textures.qrc @@ -0,0 +1,5 @@ + + + cube.png + + diff --git a/YUViewLib/shaders/fshader.glsl b/YUViewLib/shaders/fshader.glsl new file mode 100644 index 000000000..18068cf0e --- /dev/null +++ b/YUViewLib/shaders/fshader.glsl @@ -0,0 +1,18 @@ +#ifdef GL_ES +// Set default precision to medium +precision mediump int; +precision mediump float; +#endif + +uniform sampler2D texture; + +varying vec2 v_texcoord; + +//! [0] +void main() +{ + // Set fragment color from texture + gl_FragColor = texture2D(texture, v_texcoord); +} +//! [0] + diff --git a/YUViewLib/shaders/shaders.qrc b/YUViewLib/shaders/shaders.qrc new file mode 100644 index 000000000..14e02aeb3 --- /dev/null +++ b/YUViewLib/shaders/shaders.qrc @@ -0,0 +1,6 @@ + + + fshader.glsl + vshader.glsl + + diff --git a/YUViewLib/shaders/vshader.glsl b/YUViewLib/shaders/vshader.glsl new file mode 100644 index 000000000..cfdc06185 --- /dev/null +++ b/YUViewLib/shaders/vshader.glsl @@ -0,0 +1,24 @@ +#ifdef GL_ES +// Set default precision to medium +precision mediump int; +precision mediump float; +#endif + +uniform mat4 mvp_matrix; + +attribute vec4 a_position; +attribute vec2 a_texcoord; + +varying vec2 v_texcoord; + +//! [0] +void main() +{ + // Calculate vertex position in screen space + gl_Position = mvp_matrix * a_position; + + // Pass texture coordinate to fragment shader + // Value will be automatically interpolated to fragments inside polygon faces + v_texcoord = a_texcoord; +} +//! [0] diff --git a/YUViewLib/src/ui/mainwindow.cpp b/YUViewLib/src/ui/mainwindow.cpp index d4bf1229d..d4778c6c8 100644 --- a/YUViewLib/src/ui/mainwindow.cpp +++ b/YUViewLib/src/ui/mainwindow.cpp @@ -76,7 +76,7 @@ MainWindow::MainWindow(bool useAlternativeSources, QWidget *parent) : QMainWindo connect(ui.displaySplitView, &splitViewWidget::signalToggleFullScreen, this, &MainWindow::toggleFullscreen); // Setup primary/separate splitView - ui.displaySplitView->addSlaveView(&separateViewWindow.splitView); +// ui.displaySplitView->addSlaveView(&separateViewWindow.splitView); connect(ui.displaySplitView, &splitViewWidget::signalShowSeparateWindow, &separateViewWindow, &QWidget::setVisible); // Connect the playlistWidget signals to some slots @@ -109,14 +109,14 @@ MainWindow::MainWindow(bool useAlternativeSources, QWidget *parent) : QMainWindo createMenusAndActions(); - ui.playbackController->setSplitViews(ui.displaySplitView, &separateViewWindow.splitView); +// ui.playbackController->setSplitViews(ui.displaySplitView, &separateViewWindow.splitView); ui.playbackController->setPlaylist(ui.playlistTreeWidget); ui.displaySplitView->setPlaybackController(ui.playbackController); ui.displaySplitView->setPlaylistTreeWidget(ui.playlistTreeWidget); ui.displaySplitView->setVideoCache(cache.data()); ui.cachingInfoWidget->setPlaylistAndCache(ui.playlistTreeWidget, cache.data()); - separateViewWindow.splitView.setPlaybackController(ui.playbackController); - separateViewWindow.splitView.setPlaylistTreeWidget(ui.playlistTreeWidget); +// separateViewWindow.splitView.setPlaybackController(ui.playbackController); +// separateViewWindow.splitView.setPlaylistTreeWidget(ui.playlistTreeWidget); if (!settings.contains("mainWindow/geometry")) // There is no previously saved window layout. This is possibly the first time YUView is started. @@ -138,7 +138,7 @@ MainWindow::MainWindow(bool useAlternativeSources, QWidget *parent) : QMainWindo connect(&separateViewWindow, &SeparateWindow::unhandledKeyPress, this, &MainWindow::handleKeyPressFromSeparateView); // Set the controls in the state handler. This way, the state handler can save/load the current state of the view. - stateHandler.setConctrols(ui.playbackController, ui.playlistTreeWidget, ui.displaySplitView, &separateViewWindow.splitView); +// stateHandler.setConctrols(ui.playbackController, ui.playlistTreeWidget, ui.displaySplitView, &separateViewWindow.splitView); // Give the playlist a pointer to the state handler so it can save the states ti playlist ui.playlistTreeWidget->setViewStateHandler(&stateHandler); @@ -633,7 +633,7 @@ void MainWindow::updateSettings() qApp->setStyleSheet(styleSheet); ui.displaySplitView->updateSettings(); - separateViewWindow.splitView.updateSettings(); +// separateViewWindow.splitView.updateSettings(); ui.playlistTreeWidget->updateSettings(); ui.bitstreamAnalysis->updateSettings(); cache->updateSettings(); diff --git a/YUViewLib/src/ui/separateWindow.cpp b/YUViewLib/src/ui/separateWindow.cpp index 531ed3bd7..b8d7b27c2 100644 --- a/YUViewLib/src/ui/separateWindow.cpp +++ b/YUViewLib/src/ui/separateWindow.cpp @@ -35,13 +35,34 @@ #include SeparateWindow::SeparateWindow() : - splitView(this) + openGLView(this) { - setCentralWidget(&splitView); - splitView.setAttribute(Qt::WA_AcceptTouchEvents); - connect(&splitView, &splitViewWidget::signalToggleFullScreen, this, &SeparateWindow::toggleFullscreen); - connect(&splitView, &splitViewWidget::signalShowSeparateWindow, this, &SeparateWindow::splitViewShowSeparateWindow); + QSurfaceFormat format; + // asks for a OpenGL 3.2 debug context using the Core profile + format.setMajorVersion(3); + format.setMinorVersion(2); + format.setProfile(QSurfaceFormat::CoreProfile); + format.setOption(QSurfaceFormat::DebugContext); + + QOpenGLContext *context = new QOpenGLContext; + context->setFormat(format); + context->create(); + + + QOpenGLContext *ctx = QOpenGLContext::currentContext(); + logger = new QOpenGLDebugLogger(this); + + logger->initialize(); // initializes in the current context, i.e. ctx + + connect(logger, &QOpenGLDebugLogger::messageLogged, this, &SeparateWindow::handleOepnGLLoggerMessages); + logger->startLogging(); + +// setCentralWidget(&splitView); +// splitView.setAttribute(Qt::WA_AcceptTouchEvents); + +// connect(&splitView, &splitViewWidget::signalToggleFullScreen, this, &SeparateWindow::toggleFullscreen); +// connect(&splitView, &splitViewWidget::signalShowSeparateWindow, this, &SeparateWindow::splitViewShowSeparateWindow); } void SeparateWindow::toggleFullscreen() @@ -64,6 +85,11 @@ void SeparateWindow::toggleFullscreen() } } +void SeparateWindow::handleOepnGLLoggerMessages( QOpenGLDebugMessage message ) +{ + qDebug() << message; +} + void SeparateWindow::closeEvent(QCloseEvent *event) { // This window cannot be closed. Signal that we want to go to single window mode. @@ -87,9 +113,9 @@ void SeparateWindow::keyPressEvent(QKeyEvent *event) else { // See if the split view widget handles this key press. If not, pass the event on to the QWidget. - if (!splitView.handleKeyPress(event)) - emit unhandledKeyPress(event); +// if (!splitView.handleKeyPress(event)) +// emit unhandledKeyPress(event); //QWidget::keyPressEvent(event); } -} \ No newline at end of file +} diff --git a/YUViewLib/src/ui/separateWindow.h b/YUViewLib/src/ui/separateWindow.h index e9bb7fbd8..6817e1806 100644 --- a/YUViewLib/src/ui/separateWindow.h +++ b/YUViewLib/src/ui/separateWindow.h @@ -35,6 +35,9 @@ #include #include "views/splitViewWidget.h" +#include "views/openGLViewWidget.h" + +#include class SeparateWindow : public QMainWindow { @@ -42,7 +45,7 @@ class SeparateWindow : public QMainWindow public: explicit SeparateWindow(); - splitViewWidget splitView; + OpenGLViewWidget openGLView; signals: // Signal that the user wants to go back to single window mode @@ -54,6 +57,7 @@ class SeparateWindow : public QMainWindow public slots: void toggleFullscreen(); + void handleOepnGLLoggerMessages( QOpenGLDebugMessage message ); protected: void closeEvent(QCloseEvent *event) Q_DECL_OVERRIDE; @@ -65,4 +69,6 @@ protected slots: private: // If the window is shown full screen, this saves if it was maximized before going to full screen bool showNormalMaximized; + QOpenGLDebugLogger *logger; + }; diff --git a/YUViewLib/src/ui/views/openGLViewWidget.cpp b/YUViewLib/src/ui/views/openGLViewWidget.cpp new file mode 100644 index 000000000..3ae4fc2b9 --- /dev/null +++ b/YUViewLib/src/ui/views/openGLViewWidget.cpp @@ -0,0 +1,330 @@ + +#include "openGLViewWidget.h" + +#include + + +#include +#include + +#include + +OpenGLViewWidget::~OpenGLViewWidget() +{ + // Make sure the context is current when deleting the texture + // and the buffers. + makeCurrent(); + delete texture; + delete geometries; + doneCurrent(); +} + +//! [0] +void OpenGLViewWidget::mousePressEvent(QMouseEvent *e) +{ + // Save mouse press position + mousePressPosition = QVector2D(e->localPos()); +} + +void OpenGLViewWidget::mouseReleaseEvent(QMouseEvent *e) +{ + // Mouse release position - mouse press position + QVector2D diff = QVector2D(e->localPos()) - mousePressPosition; + + // Rotation axis is perpendicular to the mouse position difference + // vector + QVector3D n = QVector3D(diff.y(), diff.x(), 0.0).normalized(); + + // Accelerate angular speed relative to the length of the mouse sweep + qreal acc = diff.length() / 100.0; + + // Calculate new rotation axis as weighted sum + rotationAxis = (rotationAxis * angularSpeed + n * acc).normalized(); + + // Increase angular speed + angularSpeed += acc; +} +//! [0] + +//! [1] +void OpenGLViewWidget::timerEvent(QTimerEvent *) +{ + // Decrease angular speed (friction) + angularSpeed *= 0.99; + + // Stop rotation when speed goes below threshold + if (angularSpeed < 0.01) { + angularSpeed = 0.0; + } else { + // Update rotation + rotation = QQuaternion::fromAxisAndAngle(rotationAxis, angularSpeed) * rotation; + + // Request an update + update(); + } +} +//! [1] + +void OpenGLViewWidget::initializeGL() +{ + initializeOpenGLFunctions(); + + glClearColor(0, 0, 0, 1); + + initShaders(); + initTextures(); + +//! [2] + // Enable depth buffer + glEnable(GL_DEPTH_TEST); + + // Enable back face culling + glEnable(GL_CULL_FACE); +//! [2] + + geometries = new GeometryEngine; + + logger = new QOpenGLDebugLogger(this); + + logger->initialize(); // initializes in the current context, i.e. ctx + + connect(logger, &QOpenGLDebugLogger::messageLogged, this, &OpenGLViewWidget::handleOepnGLLoggerMessages); + logger->startLogging(); + + // Use QBasicTimer because its faster than QTimer + timer.start(12, this); + +} + + +void OpenGLViewWidget::handleOepnGLLoggerMessages( QOpenGLDebugMessage message ) +{ + qDebug() << message; +} + +//! [3] +void OpenGLViewWidget::initShaders() +{ + +// file:///home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/fshader.glsl +// file:///home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/vshader.glsl + +// QFile file(":/vshader.glsl"); + QFile file("/home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/vshader.glsl"); + + bool t = file.exists(); + QByteArray test = file.readAll(); + + // Compile vertex shader +// if (!program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vshader.glsl")) + if (!program.addShaderFromSourceFile(QOpenGLShader::Vertex, "/home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/vshader.glsl")) + close(); + + // Compile fragment shader + if (!program.addShaderFromSourceFile(QOpenGLShader::Fragment, "/home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/fshader.glsl")) +// if (!program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fshader.glsl")) + close(); + + // Link shader pipeline + if (!program.link()) + close(); + + // Bind shader pipeline for use + if (!program.bind()) + close(); +} +//! [3] + +//! [4] +void OpenGLViewWidget::initTextures() +{ + // Load cube.png image +// texture = new QOpenGLTexture(QImage(":/cube.png").mirrored()); + texture = new QOpenGLTexture(QImage("/home/sauer/Software/YUViewStuff/YUView/YUViewLib/images/cube.png").mirrored()); +// file:///home/sauer/Software/YUViewStuff/YUView/YUViewLib/images/cube.png + + // Set nearest filtering mode for texture minification + texture->setMinificationFilter(QOpenGLTexture::Nearest); + + // Set bilinear filtering mode for texture magnification + texture->setMagnificationFilter(QOpenGLTexture::Linear); + + // Wrap texture coordinates by repeating + // f.ex. texture coordinate (1.1, 1.2) is same as (0.1, 0.2) + texture->setWrapMode(QOpenGLTexture::Repeat); +} +//! [4] + +//! [5] +void OpenGLViewWidget::resizeGL(int w, int h) +{ + // Calculate aspect ratio + qreal aspect = qreal(w) / qreal(h ? h : 1); + + // Set near plane to 3.0, far plane to 7.0, field of view 45 degrees + const qreal zNear = 3.0, zFar = 7.0, fov = 45.0; + + // Reset projection + projection.setToIdentity(); + + // Set perspective projection + projection.perspective(fov, aspect, zNear, zFar); +} +//! [5] + +void OpenGLViewWidget::paintGL() +{ + // Clear color and depth buffer + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + texture->bind(); + +//! [6] + // Calculate model view transformation + QMatrix4x4 matrix; + matrix.translate(0.0, 0.0, -5.0); + matrix.rotate(rotation); + + // Set modelview-projection matrix + program.setUniformValue("mvp_matrix", projection * matrix); +//! [6] + + // Use texture unit 0 which contains cube.png + program.setUniformValue("texture", 0); + + // Draw cube geometry + geometries->drawCubeGeometry(&program); +} + + + + + + + + + +struct VertexData +{ + QVector3D position; + QVector2D texCoord; +}; + +//! [0] +GeometryEngine::GeometryEngine() + : indexBuf(QOpenGLBuffer::IndexBuffer) +{ + initializeOpenGLFunctions(); + + // Generate 2 VBOs + arrayBuf.create(); + indexBuf.create(); + + // Initializes cube geometry and transfers it to VBOs + initCubeGeometry(); +} + +GeometryEngine::~GeometryEngine() +{ + arrayBuf.destroy(); + indexBuf.destroy(); +} +//! [0] + +void GeometryEngine::initCubeGeometry() +{ + // For cube we would need only 8 vertices but we have to + // duplicate vertex for each face because texture coordinate + // is different. + VertexData vertices[] = { + // Vertex data for face 0 + {QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(0.0f, 0.0f)}, // v0 + {QVector3D( 1.0f, -1.0f, 1.0f), QVector2D(0.33f, 0.0f)}, // v1 + {QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(0.0f, 0.5f)}, // v2 + {QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v3 + + // Vertex data for face 1 + {QVector3D( 1.0f, -1.0f, 1.0f), QVector2D( 0.0f, 0.5f)}, // v4 + {QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.33f, 0.5f)}, // v5 + {QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.0f, 1.0f)}, // v6 + {QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.33f, 1.0f)}, // v7 + + // Vertex data for face 2 + {QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.5f)}, // v8 + {QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(1.0f, 0.5f)}, // v9 + {QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.66f, 1.0f)}, // v10 + {QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(1.0f, 1.0f)}, // v11 + + // Vertex data for face 3 + {QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.0f)}, // v12 + {QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(1.0f, 0.0f)}, // v13 + {QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(0.66f, 0.5f)}, // v14 + {QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(1.0f, 0.5f)}, // v15 + + // Vertex data for face 4 + {QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(0.33f, 0.0f)}, // v16 + {QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.0f)}, // v17 + {QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v18 + {QVector3D( 1.0f, -1.0f, 1.0f), QVector2D(0.66f, 0.5f)}, // v19 + + // Vertex data for face 5 + {QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v20 + {QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.66f, 0.5f)}, // v21 + {QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(0.33f, 1.0f)}, // v22 + {QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.66f, 1.0f)} // v23 + }; + + // Indices for drawing cube faces using triangle strips. + // Triangle strips can be connected by duplicating indices + // between the strips. If connecting strips have opposite + // vertex order then last index of the first strip and first + // index of the second strip needs to be duplicated. If + // connecting strips have same vertex order then only last + // index of the first strip needs to be duplicated. + GLushort indices[] = { + 0, 1, 2, 3, 3, // Face 0 - triangle strip ( v0, v1, v2, v3) + 4, 4, 5, 6, 7, 7, // Face 1 - triangle strip ( v4, v5, v6, v7) + 8, 8, 9, 10, 11, 11, // Face 2 - triangle strip ( v8, v9, v10, v11) + 12, 12, 13, 14, 15, 15, // Face 3 - triangle strip (v12, v13, v14, v15) + 16, 16, 17, 18, 19, 19, // Face 4 - triangle strip (v16, v17, v18, v19) + 20, 20, 21, 22, 23 // Face 5 - triangle strip (v20, v21, v22, v23) + }; + +//! [1] + // Transfer vertex data to VBO 0 + arrayBuf.bind(); + arrayBuf.allocate(vertices, 24 * sizeof(VertexData)); + + // Transfer index data to VBO 1 + indexBuf.bind(); + indexBuf.allocate(indices, 34 * sizeof(GLushort)); +//! [1] +} + +//! [2] +void GeometryEngine::drawCubeGeometry(QOpenGLShaderProgram *program) +{ + // Tell OpenGL which VBOs to use + arrayBuf.bind(); + indexBuf.bind(); + + // Offset for position + quintptr offset = 0; + + // Tell OpenGL programmable pipeline how to locate vertex position data + int vertexLocation = program->attributeLocation("a_position"); + program->enableAttributeArray(vertexLocation); + program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 3, sizeof(VertexData)); + + // Offset for texture coordinate + offset += sizeof(QVector3D); + + // Tell OpenGL programmable pipeline how to locate vertex texture coordinate data + int texcoordLocation = program->attributeLocation("a_texcoord"); + program->enableAttributeArray(texcoordLocation); + program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData)); + + // Draw cube geometry using indices from VBO 1 + glDrawElements(GL_TRIANGLE_STRIP, 34, GL_UNSIGNED_SHORT, nullptr); +} +//! [2] diff --git a/YUViewLib/src/ui/views/openGLViewWidget.h b/YUViewLib/src/ui/views/openGLViewWidget.h new file mode 100644 index 000000000..48a146c4c --- /dev/null +++ b/YUViewLib/src/ui/views/openGLViewWidget.h @@ -0,0 +1,110 @@ +/* This file is part of YUView - The YUV player with advanced analytics toolset +* +* Copyright (C) 2015 Institut für Nachrichtentechnik, RWTH Aachen University, GERMANY +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 3 of the License, or +* (at your option) any later version. +* +* In addition, as a special exception, the copyright holders give +* permission to link the code of portions of this program with the +* OpenSSL library under certain conditions as described in each +* individual source file, and distribute linked combinations including +* the two. +* +* You must obey the GNU General Public License in all respects for all +* of the code used other than OpenSSL. If you modify file(s) with this +* exception, you may extend this exception to your version of the +* file(s), but you are not obligated to do so. If you do not wish to do +* so, delete this exception statement from your version. If you delete +* this exception statement from all source files in the program, then +* also delete it here. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +#ifndef OPENGLVIEWWIDGET_H +#define OPENGLVIEWWIDGET_H + + +#include +#include +#include + + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class GeometryEngine : protected QOpenGLFunctions +{ +public: + GeometryEngine(); + virtual ~GeometryEngine(); + + void drawCubeGeometry(QOpenGLShaderProgram *program); + +private: + void initCubeGeometry(); + + QOpenGLBuffer arrayBuf; + QOpenGLBuffer indexBuf; +}; + + +class OpenGLViewWidget : public QOpenGLWidget, protected QOpenGLFunctions +{ + Q_OBJECT + +public: + using QOpenGLWidget::QOpenGLWidget; + ~OpenGLViewWidget(); + +public slots: + void handleOepnGLLoggerMessages( QOpenGLDebugMessage message ); + +protected: + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void timerEvent(QTimerEvent *e) override; + + void initializeGL() override; + void resizeGL(int w, int h) override; + void paintGL() override; + + void initShaders(); + void initTextures(); + +private: + QBasicTimer timer; + QOpenGLShaderProgram program; + GeometryEngine *geometries = nullptr; + + QOpenGLTexture *texture = nullptr; + + QMatrix4x4 projection; + + QVector2D mousePressPosition; + QVector3D rotationAxis; + qreal angularSpeed = 0; + QQuaternion rotation; + + + QOpenGLDebugLogger *logger; + +}; + +#endif // OPENGLVIEWWIDGET_H From 0831e1794f179a31862733e10e1ce142eae743d1 Mon Sep 17 00:00:00 2001 From: Johannes Sauer Date: Fri, 2 Oct 2020 14:08:13 +0200 Subject: [PATCH 02/29] made openglwidget central widget. it now renders properly on the separate window --- YUViewLib/src/ui/separateWindow.cpp | 44 ++++++++++++++------- YUViewLib/src/ui/views/openGLViewWidget.cpp | 14 +++---- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/YUViewLib/src/ui/separateWindow.cpp b/YUViewLib/src/ui/separateWindow.cpp index b8d7b27c2..53ac6489e 100644 --- a/YUViewLib/src/ui/separateWindow.cpp +++ b/YUViewLib/src/ui/separateWindow.cpp @@ -38,27 +38,41 @@ SeparateWindow::SeparateWindow() : openGLView(this) { - QSurfaceFormat format; - // asks for a OpenGL 3.2 debug context using the Core profile - format.setMajorVersion(3); - format.setMinorVersion(2); - format.setProfile(QSurfaceFormat::CoreProfile); - format.setOption(QSurfaceFormat::DebugContext); - QOpenGLContext *context = new QOpenGLContext; - context->setFormat(format); - context->create(); + // https://doc.qt.io/qt-5/qopenglwidget.html + //As described above, it is simpler and more robust to set the requested format globally so + // that it applies to all windows and contexts during the lifetime of the application. + QSurfaceFormat format; + // asks for a OpenGL 3.2 debug context using the Core profile + format.setMajorVersion(3); + format.setMinorVersion(2); + format.setDepthBufferSize(24); + format.setStencilBufferSize(8); + format.setProfile(QSurfaceFormat::CoreProfile); + format.setOption(QSurfaceFormat::DebugContext); + QSurfaceFormat::setDefaultFormat(format); +// QSurfaceFormat format; +// // asks for a OpenGL 3.2 debug context using the Core profile +// format.setMajorVersion(3); +// format.setMinorVersion(2); +// format.setProfile(QSurfaceFormat::CoreProfile); +// format.setOption(QSurfaceFormat::DebugContext); - QOpenGLContext *ctx = QOpenGLContext::currentContext(); - logger = new QOpenGLDebugLogger(this); +// QOpenGLContext *context = new QOpenGLContext; +// context->setFormat(format); +// context->create(); - logger->initialize(); // initializes in the current context, i.e. ctx - connect(logger, &QOpenGLDebugLogger::messageLogged, this, &SeparateWindow::handleOepnGLLoggerMessages); - logger->startLogging(); +// QOpenGLContext *ctx = QOpenGLContext::currentContext(); +// logger = new QOpenGLDebugLogger(this); -// setCentralWidget(&splitView); +// logger->initialize(); // initializes in the current context, i.e. ctx + +// connect(logger, &QOpenGLDebugLogger::messageLogged, this, &SeparateWindow::handleOepnGLLoggerMessages); +// logger->startLogging(); + + setCentralWidget(&openGLView); // splitView.setAttribute(Qt::WA_AcceptTouchEvents); // connect(&splitView, &splitViewWidget::signalToggleFullScreen, this, &SeparateWindow::toggleFullscreen); diff --git a/YUViewLib/src/ui/views/openGLViewWidget.cpp b/YUViewLib/src/ui/views/openGLViewWidget.cpp index 3ae4fc2b9..02e16b382 100644 --- a/YUViewLib/src/ui/views/openGLViewWidget.cpp +++ b/YUViewLib/src/ui/views/openGLViewWidget.cpp @@ -67,6 +67,13 @@ void OpenGLViewWidget::timerEvent(QTimerEvent *) void OpenGLViewWidget::initializeGL() { + logger = new QOpenGLDebugLogger(this); + + logger->initialize(); // initializes in the current context, i.e. ctx + + connect(logger, &QOpenGLDebugLogger::messageLogged, this, &OpenGLViewWidget::handleOepnGLLoggerMessages); + logger->startLogging(); + initializeOpenGLFunctions(); glClearColor(0, 0, 0, 1); @@ -84,13 +91,6 @@ void OpenGLViewWidget::initializeGL() geometries = new GeometryEngine; - logger = new QOpenGLDebugLogger(this); - - logger->initialize(); // initializes in the current context, i.e. ctx - - connect(logger, &QOpenGLDebugLogger::messageLogged, this, &OpenGLViewWidget::handleOepnGLLoggerMessages); - logger->startLogging(); - // Use QBasicTimer because its faster than QTimer timer.start(12, this); From e1e72d19edda9bc64dc41972963aeb86fd7a5cab Mon Sep 17 00:00:00 2001 From: Johannes Sauer Date: Mon, 5 Oct 2020 11:58:29 +0200 Subject: [PATCH 03/29] basic openGl backend works. can read a frame from a hardcoded sequence and display it. needs to be connected to the rest of YUView. should be come a separate new window, atm replaced/hacked the old separate window mode. qt resources system not working yet --- YUViewLib/shaders/fragmentYUV2RGBshader.glsl | 47 ++ YUViewLib/shaders/shaders.qrc | 2 + YUViewLib/shaders/vertexshader.glsl | 32 + YUViewLib/src/ui/separateWindow.cpp | 4 +- YUViewLib/src/ui/views/openGLViewWidget.cpp | 765 ++++++++++++++----- YUViewLib/src/ui/views/openGLViewWidget.h | 73 +- 6 files changed, 715 insertions(+), 208 deletions(-) create mode 100644 YUViewLib/shaders/fragmentYUV2RGBshader.glsl create mode 100644 YUViewLib/shaders/vertexshader.glsl diff --git a/YUViewLib/shaders/fragmentYUV2RGBshader.glsl b/YUViewLib/shaders/fragmentYUV2RGBshader.glsl new file mode 100644 index 000000000..c8ac01bed --- /dev/null +++ b/YUViewLib/shaders/fragmentYUV2RGBshader.glsl @@ -0,0 +1,47 @@ +#version 330 core + +// Interpolated values from the vertex shaders +in vec2 fragmentUVLuma; +//in vec2 fragmentUVChroma; + +// Ouput data +out vec4 color_0; +//out vec3 color_1; + +// Values that stay constant for the whole mesh. +uniform sampler2D textureSamplerRed; +uniform sampler2D textureSamplerGreen; +uniform sampler2D textureSamplerBlue; + + +void main(){ + // ITU-R BT 709 to RGB mapping matrix + mat3 mat709toRGB = mat3(1.164, 1.164, 1.164, // definition looks transposed + 0.0, -0.213, 2.112, // since openGL uses column-major order + 1.793, -0.533, 0.0); + + + + // Output color = color of the texture at the specified UV + vec3 color709; + //color709.rgb = texture2D( textureSamplerYUV, fragmentUV ).rgb; + color709.r = texture2D( textureSamplerRed, fragmentUVLuma ).r; + color709.g = texture2D( textureSamplerGreen, fragmentUVLuma ).r; + color709.b = texture2D( textureSamplerBlue, fragmentUVLuma ).r; + + + // make sure input has correct range + color709.r = clamp(color709.r, 16.0/255, 235.0/255); // Y + color709.g = clamp(color709.g, 16.0/255, 240.0/255); // U + color709.b = clamp(color709.b, 16.0/255, 240.0/255); // V + + // de/normalization + color709.r = color709.r - 0.0625; + color709.g = color709.g - 0.5; + color709.b = color709.b - 0.5; + // finally the conversion + color_0 = vec4(clamp(mat709toRGB * color709, 0.0, 1.0), 1.0f); + //color_1.r = 0.5; + //color_1.g = 0.7; + //color_1.b = 0.8; +} diff --git a/YUViewLib/shaders/shaders.qrc b/YUViewLib/shaders/shaders.qrc index 14e02aeb3..0dafb214b 100644 --- a/YUViewLib/shaders/shaders.qrc +++ b/YUViewLib/shaders/shaders.qrc @@ -2,5 +2,7 @@ fshader.glsl vshader.glsl + fragmentYUV2RGBshader.glsl + vertexshader.glsl diff --git a/YUViewLib/shaders/vertexshader.glsl b/YUViewLib/shaders/vertexshader.glsl new file mode 100644 index 000000000..fd4888865 --- /dev/null +++ b/YUViewLib/shaders/vertexshader.glsl @@ -0,0 +1,32 @@ +#version 330 core + +// Input vertex data, different for all executions of this shader. +layout(location = 0) in vec3 vertexPosition_modelspace; +layout(location = 1) in vec2 vertexLuma; +layout(location = 2) in vec2 vertexChroma; + +// Output data ; will be interpolated for each fragment. +out vec2 fragmentUVLuma; +//out vec2 fragmentUVChroma; + +// Values that stay constant for the whole mesh. +uniform mat4 MVP; + +void main(){ + + gl_Position = vec4(vertexPosition_modelspace, 1.0f); + + // Projected position in 3D homogeneous coordinates + /*vec4 ref_position_world = vec4(vertexPosition_modelspace[0],vertexPosition_modelspace[1],1,1.0); + + + + gl_Position = MVP * ref_position_world; + /// putting -1.0 there mirrors the image so it is correct. should not be necessary. how to fix? + gl_Position[0] = gl_Position[0]/gl_Position[3]; + gl_Position[1] = gl_Position[1]/gl_Position[3]; + gl_Position[2] = gl_Position[2]/gl_Position[3]; + gl_Position[3] = 1.0;*/ + fragmentUVLuma = vec2(vertexLuma.x, 1.0 - vertexLuma.y); + //fragmentUVChroma = vec2(vertexChroma.x, 1.0 - vertexChroma.y); +} diff --git a/YUViewLib/src/ui/separateWindow.cpp b/YUViewLib/src/ui/separateWindow.cpp index 53ac6489e..033eda4d8 100644 --- a/YUViewLib/src/ui/separateWindow.cpp +++ b/YUViewLib/src/ui/separateWindow.cpp @@ -72,9 +72,11 @@ SeparateWindow::SeparateWindow() : // connect(logger, &QOpenGLDebugLogger::messageLogged, this, &SeparateWindow::handleOepnGLLoggerMessages); // logger->startLogging(); - setCentralWidget(&openGLView); + setCentralWidget(&openGLView); // splitView.setAttribute(Qt::WA_AcceptTouchEvents); + + // connect(&splitView, &splitViewWidget::signalToggleFullScreen, this, &SeparateWindow::toggleFullscreen); // connect(&splitView, &splitViewWidget::signalShowSeparateWindow, this, &SeparateWindow::splitViewShowSeparateWindow); } diff --git a/YUViewLib/src/ui/views/openGLViewWidget.cpp b/YUViewLib/src/ui/views/openGLViewWidget.cpp index 02e16b382..565b5679b 100644 --- a/YUViewLib/src/ui/views/openGLViewWidget.cpp +++ b/YUViewLib/src/ui/views/openGLViewWidget.cpp @@ -8,6 +8,40 @@ #include #include +// Include GLM +#include +#include +#include + + +OpenGLViewWidget::OpenGLViewWidget(QWidget *parent) + : QOpenGLWidget(parent), + m_frameWidth(1), + m_frameHeight(1), + m_vertice_indices_Vbo(QOpenGLBuffer::IndexBuffer), + m_texture_Ydata(0), + m_texture_Udata(0), + m_texture_Vdata(0), + m_program(0) +{ +// m_core = QCoreApplication::arguments().contains(QStringLiteral("--coreprofile")); +// // --transparent causes the clear color to be transparent. Therefore, on systems that +// // support it, the widget will become transparent apart from the logo. +// m_transparent = QCoreApplication::arguments().contains(QStringLiteral("--transparent")); +// if (m_transparent) +// setAttribute(Qt::WA_TranslucentBackground); + +// QSizePolicy p(sizePolicy()); +//// p.setHorizontalPolicy(QSizePolicy::Fixed); +//// p.setVerticalPolicy(QSizePolicy::Fixed); +// p.setHeightForWidth(true); +// setSizePolicy(p); + + +// uncommenting this enables update on the vertical refresh of the monitor. however it somehow interferes with the file dialog +// connect(this,SIGNAL(frameSwapped()),this,SLOT(update())); + +} OpenGLViewWidget::~OpenGLViewWidget() { @@ -15,7 +49,6 @@ OpenGLViewWidget::~OpenGLViewWidget() // and the buffers. makeCurrent(); delete texture; - delete geometries; doneCurrent(); } @@ -65,266 +98,616 @@ void OpenGLViewWidget::timerEvent(QTimerEvent *) } //! [1] -void OpenGLViewWidget::initializeGL() +void OpenGLViewWidget::handleOepnGLLoggerMessages( QOpenGLDebugMessage message ) { - logger = new QOpenGLDebugLogger(this); + qDebug() << message; +} - logger->initialize(); // initializes in the current context, i.e. ctx +////! [3] +//void OpenGLViewWidget::initShaders() +//{ + +//// file:///home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/fshader.glsl +//// file:///home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/vshader.glsl + +//// QFile file(":/vshader.glsl"); +// QFile file("/home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/vshader.glsl"); + +//// bool t = file.exists(); +//// QByteArray test = file.readAll(); + +// // Compile vertex shader +//// if (!program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vshader.glsl")) +// if (!program.addShaderFromSourceFile(QOpenGLShader::Vertex, "/home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/vshader.glsl")) +// close(); + +// // Compile fragment shader +// if (!program.addShaderFromSourceFile(QOpenGLShader::Fragment, "/home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/fshader.glsl")) +//// if (!program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fshader.glsl")) +// close(); + +// // Link shader pipeline +// if (!program.link()) +// close(); + +// // Bind shader pipeline for use +// if (!program.bind()) +// close(); +//} +////! [3] + +////! [4] +//void OpenGLViewWidget::initTextures() +//{ +// // Load cube.png image +//// texture = new QOpenGLTexture(QImage(":/cube.png").mirrored()); +// texture = new QOpenGLTexture(QImage("/home/sauer/Software/YUViewStuff/YUView/YUViewLib/images/cube.png").mirrored()); +//// file:///home/sauer/Software/YUViewStuff/YUView/YUViewLib/images/cube.png + +// // Set nearest filtering mode for texture minification +// texture->setMinificationFilter(QOpenGLTexture::Nearest); + +// // Set bilinear filtering mode for texture magnification +// texture->setMagnificationFilter(QOpenGLTexture::Linear); + +// // Wrap texture coordinates by repeating +// // f.ex. texture coordinate (1.1, 1.2) is same as (0.1, 0.2) +// texture->setWrapMode(QOpenGLTexture::Repeat); +//} +//! [4] - connect(logger, &QOpenGLDebugLogger::messageLogged, this, &OpenGLViewWidget::handleOepnGLLoggerMessages); - logger->startLogging(); +//! [5] +void OpenGLViewWidget::resizeGL(int w, int h) +{ + // Calculate aspect ratio + qreal aspect = qreal(w) / qreal(h ? h : 1); - initializeOpenGLFunctions(); + // Set near plane to 3.0, far plane to 7.0, field of view 45 degrees + const qreal zNear = 3.0, zFar = 7.0, fov = 45.0; - glClearColor(0, 0, 0, 1); + // Reset projection + projection.setToIdentity(); - initShaders(); - initTextures(); + // Set perspective projection + projection.perspective(fov, aspect, zNear, zFar); +} +//! [5] -//! [2] - // Enable depth buffer - glEnable(GL_DEPTH_TEST); +//void OpenGLViewWidget::paintGL() +//{ +// // Clear color and depth buffer +// glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - // Enable back face culling - glEnable(GL_CULL_FACE); -//! [2] +// texture->bind(); - geometries = new GeometryEngine; +////! [6] +// // Calculate model view transformation +// QMatrix4x4 matrix; +// matrix.translate(0.0, 0.0, -5.0); +// matrix.rotate(rotation); - // Use QBasicTimer because its faster than QTimer - timer.start(12, this); +// // Set modelview-projection matrix +// program.setUniformValue("mvp_matrix", projection * matrix); +////! [6] -} +// // Use texture unit 0 which contains cube.png +// program.setUniformValue("texture", 0); +// // Draw cube geometry +// geometries->drawCubeGeometry(&program); +//} -void OpenGLViewWidget::handleOepnGLLoggerMessages( QOpenGLDebugMessage message ) + + + + + + + + +struct VertexData { - qDebug() << message; -} + QVector3D position; + QVector2D texCoord; +}; -//! [3] -void OpenGLViewWidget::initShaders() + +void OpenGLViewWidget::updateFrame(const QByteArray &textureData) { + if(!m_pixelFormat.isValid() || textureData.isEmpty()) + { + qDebug() << "Unvalid PixelFormat or empty texture Array"; + return; + } -// file:///home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/fshader.glsl -// file:///home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/vshader.glsl +// QElapsedTimer timer; +// timer.start(); -// QFile file(":/vshader.glsl"); - QFile file("/home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/vshader.glsl"); + m_swapUV = 0; // todo - bool t = file.exists(); - QByteArray test = file.readAll(); - // Compile vertex shader -// if (!program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vshader.glsl")) - if (!program.addShaderFromSourceFile(QOpenGLShader::Vertex, "/home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/vshader.glsl")) - close(); + const unsigned char *srcY = (unsigned char*)textureData.data(); + const unsigned char *srcU = srcY + m_componentLength + (m_swapUV * (m_componentLength / m_pixelFormat.getSubsamplingHor()) / m_pixelFormat.getSubsamplingVer()); + const unsigned char *srcV = srcY + m_componentLength + ((1 - m_swapUV) * (m_componentLength / m_pixelFormat.getSubsamplingHor()) / m_pixelFormat.getSubsamplingVer()); - // Compile fragment shader - if (!program.addShaderFromSourceFile(QOpenGLShader::Fragment, "/home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/fshader.glsl")) -// if (!program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fshader.glsl")) - close(); + // transmitting the YUV data as three different textures + // Y on unit 0 + m_texture_Ydata->setData(QOpenGLTexture::Red, QOpenGLTexture::UInt8, srcY); + // U on unit 1 + m_texture_Udata->setData(QOpenGLTexture::Red, QOpenGLTexture::UInt8, srcU); + // V on unit 2 + m_texture_Vdata->setData(QOpenGLTexture::Red, QOpenGLTexture::UInt8, srcV); - // Link shader pipeline - if (!program.link()) - close(); - // Bind shader pipeline for use - if (!program.bind()) - close(); +// qDebug() << "Moving data to graphics card took" << timer.elapsed() << "milliseconds"; + + update(); } -//! [3] -//! [4] -void OpenGLViewWidget::initTextures() +void OpenGLViewWidget::updateFormat(int frameWidth, int frameHeight, YUV_Internals::yuvPixelFormat PxlFormat) { - // Load cube.png image -// texture = new QOpenGLTexture(QImage(":/cube.png").mirrored()); - texture = new QOpenGLTexture(QImage("/home/sauer/Software/YUViewStuff/YUView/YUViewLib/images/cube.png").mirrored()); -// file:///home/sauer/Software/YUViewStuff/YUView/YUViewLib/images/cube.png + m_frameWidth = frameWidth; + m_frameHeight = frameHeight; + m_textureFormat = QImage::Format_RGB32; + + m_pixelFormat = PxlFormat; + + +// m_swapUV = 0; +// if(m_pixelFormat == YUVC_422YpCrCb8PlanarPixelFormat || m_pixelFormat == YUVC_444YpCrCb8PlanarPixelFormat) +// m_swapUV = 1; + + m_componentLength = m_frameWidth*m_frameHeight; + + QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); + + + + // just put video on two triangles forming a rectangle + // compute frame vertices and copy to buffer + + m_videoFrameTriangles_vertices.clear(); + m_videoFrameTriangles_vertices.push_back(glm::vec3(-1,-1,0)); + m_videoFrameTriangles_vertices.push_back(glm::vec3(1,1,0)); + m_videoFrameTriangles_vertices.push_back(glm::vec3(-1,1,0)); + m_videoFrameTriangles_vertices.push_back(glm::vec3(1,-1,0)); + + + // triangle definition using indices. + // we use GL_TRIANGLE_STRIP: Every group of 3 adjacent vertices forms a triangle. + // Hence need only 4 indices to make our rectangle for the video. + m_videoFrameTriangles_indices.clear(); + //First Triangle + m_videoFrameTriangles_indices.push_back(2); + m_videoFrameTriangles_indices.push_back(0); + m_videoFrameTriangles_indices.push_back(1); + // second triangle + m_videoFrameTriangles_indices.push_back(3); + + m_videoFrameDataPoints_Luma.clear(); + m_videoFrameDataPoints_Luma.push_back(0.f); + m_videoFrameDataPoints_Luma.push_back(0.f); + m_videoFrameDataPoints_Luma.push_back(1.f); + m_videoFrameDataPoints_Luma.push_back(1.f); + m_videoFrameDataPoints_Luma.push_back(0.f); + m_videoFrameDataPoints_Luma.push_back(1.f); + m_videoFrameDataPoints_Luma.push_back(1.f); + m_videoFrameDataPoints_Luma.push_back(0.f); + m_vertices_Vbo.create(); + m_vertices_Vbo.bind(); + m_vertices_Vbo.setUsagePattern(QOpenGLBuffer::StaticDraw); + m_vertices_Vbo.allocate(&m_videoFrameTriangles_vertices[0], m_videoFrameTriangles_vertices.size()* sizeof(glm::vec3)); + // compute indices of vertices and copy to buffer +// std::cout << "nr of triangles:" << m_videoFrameTriangles_indices.size() / 3 << std::endl; + m_vertice_indices_Vbo.create(); + m_vertice_indices_Vbo.bind(); + m_vertice_indices_Vbo.setUsagePattern(QOpenGLBuffer::StaticDraw); + m_vertice_indices_Vbo.allocate(&m_videoFrameTriangles_indices[0], m_videoFrameTriangles_indices.size() * sizeof(unsigned int)); + m_textureLuma_coordinates_Vbo.create(); + m_textureLuma_coordinates_Vbo.bind(); + m_textureLuma_coordinates_Vbo.setUsagePattern(QOpenGLBuffer::StaticDraw); + m_textureLuma_coordinates_Vbo.allocate(&m_videoFrameDataPoints_Luma[0], m_videoFrameDataPoints_Luma.size() * sizeof(float)); + + //recompute opengl matrices + + setupMatrices(); + + // setup texture objects + + // transmitting the YUV data as three different textures + // Y on unit 0 + if(m_texture_Ydata) + { + m_texture_Ydata->destroy(); + } + m_texture_Ydata = std::make_shared(QOpenGLTexture::Target2D); + m_texture_Ydata->create(); + m_texture_Ydata->setSize(m_frameWidth,m_frameHeight); + m_texture_Ydata->setFormat(QOpenGLTexture::R8_UNorm); +#if QT_VERSION >= QT_VERSION_CHECK(5,5,0) + m_texture_Ydata->allocateStorage(QOpenGLTexture::Red,QOpenGLTexture::UInt8); +#else + m_texture_Ydata->allocateStorage(); +#endif + // Set filtering modes for texture minification and magnification + m_texture_Ydata->setMinificationFilter(QOpenGLTexture::Nearest); + m_texture_Ydata->setMagnificationFilter(QOpenGLTexture::Linear); + // Wrap texture coordinates by repeating + m_texture_Ydata->setWrapMode(QOpenGLTexture::ClampToBorder); - // Set nearest filtering mode for texture minification - texture->setMinificationFilter(QOpenGLTexture::Nearest); - // Set bilinear filtering mode for texture magnification - texture->setMagnificationFilter(QOpenGLTexture::Linear); + + // U on unit 1 + if(m_texture_Udata) + { + m_texture_Udata->destroy(); + } + m_texture_Udata = std::make_shared(QOpenGLTexture::Target2D); + m_texture_Udata->create(); + m_texture_Udata->setSize(m_frameWidth/m_pixelFormat.getSubsamplingHor(),m_frameHeight/m_pixelFormat.getSubsamplingVer()); + m_texture_Udata->setFormat(QOpenGLTexture::R8_UNorm); +#if QT_VERSION >= QT_VERSION_CHECK(5,5,0) + m_texture_Udata->allocateStorage(QOpenGLTexture::Red,QOpenGLTexture::UInt8); +#else + m_texture_Udata->allocateStorage(); +#endif + // Set filtering modes for texture minification and magnification + m_texture_Udata->setMinificationFilter(QOpenGLTexture::Nearest); + m_texture_Udata->setMagnificationFilter(QOpenGLTexture::Linear); // Wrap texture coordinates by repeating - // f.ex. texture coordinate (1.1, 1.2) is same as (0.1, 0.2) - texture->setWrapMode(QOpenGLTexture::Repeat); -} -//! [4] + m_texture_Udata->setWrapMode(QOpenGLTexture::ClampToBorder); -//! [5] -void OpenGLViewWidget::resizeGL(int w, int h) -{ - // Calculate aspect ratio - qreal aspect = qreal(w) / qreal(h ? h : 1); - // Set near plane to 3.0, far plane to 7.0, field of view 45 degrees - const qreal zNear = 3.0, zFar = 7.0, fov = 45.0; + // V on unit 2 + if(m_texture_Vdata) + { + m_texture_Vdata->destroy(); + } - // Reset projection - projection.setToIdentity(); + m_texture_Vdata = std::make_shared(QOpenGLTexture::Target2D); + m_texture_Vdata->create(); + m_texture_Vdata->setSize(m_frameWidth/m_pixelFormat.getSubsamplingHor(), m_frameHeight/m_pixelFormat.getSubsamplingVer()); + m_texture_Vdata->setFormat(QOpenGLTexture::R8_UNorm); +#if QT_VERSION >= QT_VERSION_CHECK(5,5,0) + m_texture_Vdata->allocateStorage(QOpenGLTexture::Red,QOpenGLTexture::UInt8); +#else + m_texture_Vdata->allocateStorage(); +#endif + //Set filtering modes for texture minification and magnification + m_texture_Vdata->setMinificationFilter(QOpenGLTexture::Nearest); + m_texture_Vdata->setMagnificationFilter(QOpenGLTexture::Linear); + // Wrap texture coordinates by repeating + m_texture_Vdata->setWrapMode(QOpenGLTexture::ClampToBorder); + + update(); - // Set perspective projection - projection.perspective(fov, aspect, zNear, zFar); } -//! [5] -void OpenGLViewWidget::paintGL() + + +void OpenGLViewWidget::initializeGL() { - // Clear color and depth buffer - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + // In this example the widget's corresponding top-level window can change + // several times during the widget's lifetime. Whenever this happens, the + // QOpenOpenGLViewWidget's associated context is destroyed and a new one is created. + // Therefore we have to be prepared to clean up the resources on the + // aboutToBeDestroyed() signal, instead of the destructor. The emission of + // the signal will be followed by an invocation of initializeGL() where we + // can recreate all resources. +// connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &OpenGLViewWidget::cleanup); + + logger = new QOpenGLDebugLogger(this); - texture->bind(); + logger->initialize(); // initializes in the current context, i.e. ctx -//! [6] - // Calculate model view transformation - QMatrix4x4 matrix; - matrix.translate(0.0, 0.0, -5.0); - matrix.rotate(rotation); + connect(logger, &QOpenGLDebugLogger::messageLogged, this, &OpenGLViewWidget::handleOepnGLLoggerMessages); + logger->startLogging(); - // Set modelview-projection matrix - program.setUniformValue("mvp_matrix", projection * matrix); -//! [6] - // Use texture unit 0 which contains cube.png - program.setUniformValue("texture", 0); + initializeOpenGLFunctions(); + const bool is_transparent = false; + glClearColor(0, 0, 0, is_transparent ? 0 : 1); + // Dark blue background + glClearColor(0.5f, 0.5f, 0.5f, 0.0f); - // Draw cube geometry - geometries->drawCubeGeometry(&program); -} + // Enable depth test + glEnable(GL_DEPTH_TEST); + // Accept fragment if it closer to the camera than the former one + glDepthFunc(GL_LESS); + setupMatrices(); + // Create a vertex array object. In OpenGL ES 2.0 and OpenGL 2.x + // implementations this is optional and support may not be present + // at all. Nonetheless the below code works in all cases and makes + // sure there is a VAO when one is needed. + m_vao.create(); + QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); + m_program = new QOpenGLShaderProgram; +// m_program->addShaderFromSourceFile(QOpenGLShader::Vertex,":/vertexshader.glsl"); +// m_program->addShaderFromSourceFile(QOpenGLShader::Fragment,":fragmentYUV2RGBshader.glsl"); + m_program->addShaderFromSourceFile(QOpenGLShader::Vertex,"/home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/vertexshader.glsl"); + m_program->addShaderFromSourceFile(QOpenGLShader::Fragment,"/home/sauer/Software/YUViewStuff/YUView/YUViewLib/shaders/fragmentYUV2RGBshader.glsl"); + m_program->link(); + m_program->bind(); + m_matMVP_Loc = m_program->uniformLocation("MVP"); + m_program->setUniformValue("textureSamplerRed", 0); + m_program->setUniformValue("textureSamplerGreen", 1); + m_program->setUniformValue("textureSamplerBlue", 2); + m_vertices_Loc = m_program->attributeLocation("vertexPosition_modelspace"); + m_textureLuma_Loc = m_program->attributeLocation("vertexLuma"); + m_textureChroma_Loc = m_program->attributeLocation("vertexChroma"); + /*m_vertices_Loc = m_program->attributeLocation("vertexPosition_modelspace"); + m_texture_Loc = m_program->attributeLocation("vertexUV");*/ + // Create vertex buffer objects + m_vertices_Vbo.create(); + m_vertice_indices_Vbo.create(); + // m_texture_coordinates_Vbo.create(); + m_textureLuma_coordinates_Vbo.create(); + m_textureChroma_coordinates_Vbo.create(); -struct VertexData + // Store the vertex attribute bindings for the program. + setupVertexAttribs(); + + m_program->release(); + + // for debugging. use first frame of RaceHorsesL_832x480_30fps_8bit_420pf + // /home/sauer/Videos/SD/RaceHorsesL_832x480_30fps_8bit_420pf.yuv + + updateFormat(832,480, YUV_Internals::yuvPixelFormat(YUV_Internals::Subsampling::YUV_420, 8, YUV_Internals::PlaneOrder::YUV)); // YUV 4:2:0) + + QFile raceHorsesSeq("/home/sauer/Videos/SD/RaceHorsesL_832x480_30fps_8bit_420pf.yuv"); + + if (!raceHorsesSeq.open(QIODevice::ReadOnly )) + { + qDebug() << "could not open file:" << raceHorsesSeq.fileName(); + } + QByteArray firstFrame = raceHorsesSeq.read(599040); + + updateFrame(firstFrame); +} + +void OpenGLViewWidget::setupVertexAttribs() { - QVector3D position; - QVector2D texCoord; -}; -//! [0] -GeometryEngine::GeometryEngine() - : indexBuf(QOpenGLBuffer::IndexBuffer) + QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions(); + + // 1rst attribute buffer : vertices + m_vertices_Vbo.bind(); + f->glEnableVertexAttribArray(m_vertices_Loc); + f->glVertexAttribPointer( + m_vertices_Loc, // attribute. + 3, // size + GL_FLOAT, // type + GL_FALSE, // normalized? + 0, // stride + (void*)0 // array buffer offset + ); + + // 2nd attribute buffer : UVs + m_textureLuma_coordinates_Vbo.bind(); + f->glEnableVertexAttribArray(m_textureLuma_Loc); + f->glVertexAttribPointer( + m_textureLuma_Loc, // attribute. + 2, // size : U+V => 2 + GL_FLOAT, // type + GL_FALSE, // normalized? + 0, // stride + (void*)0 // array buffer offset + ); + +// // 3nd attribute buffer : UVs Chroma +// m_textureChroma_coordinates_Vbo.bind(); +// f->glEnableVertexAttribArray(m_textureChroma_Loc); +// f->glVertexAttribPointer( +// m_textureChroma_Loc, // attribute. +// 2, // size : U+V => 2 +// GL_FLOAT, // type +// GL_FALSE, // normalized? +// 0, // stride +// (void*)0 // array buffer offset +// ); + +} + +void OpenGLViewWidget::setupMatrices() { - initializeOpenGLFunctions(); + glm::mat3 K_view = glm::mat3(1.f); - // Generate 2 VBOs - arrayBuf.create(); - indexBuf.create(); + // Our ModelViewProjection : projection of model to different view and then to image + m_MVP = getProjectionFromCamCalibration(K_view,1000,1); - // Initializes cube geometry and transfers it to VBOs - initCubeGeometry(); + // std::cout << "MVP: " << glm::to_string(m_MVP) << std::endl; } -GeometryEngine::~GeometryEngine() +glm::mat4 OpenGLViewWidget::getProjectionFromCamCalibration(glm::mat3 &calibrationMatrix, float clipFar, float clipNear) { - arrayBuf.destroy(); - indexBuf.destroy(); + calibrationMatrix[0] = -1.f * calibrationMatrix[0]; + calibrationMatrix[1] = -1.f * calibrationMatrix[1]; + calibrationMatrix[2] = -1.f * calibrationMatrix[2]; + clipFar = -1.0 * clipFar; + clipNear = -1.0 * clipNear; + + glm::mat4 perspective(calibrationMatrix); + perspective[2][2] = clipNear + clipFar; + perspective[3][2] = clipNear * clipFar; + perspective[2][3] = -1.f; + perspective[3][3] = 0.f; + + glm::mat4 toNDC = glm::ortho(0.f,(float) m_frameWidth,(float) m_frameHeight,0.f,clipNear,clipFar); + + glm::mat4 Projection2 = toNDC * perspective; + + +// std::cout << "perspective: " << glm::to_string(perspective) << std::endl; +// std::cout << "toNDC: " << glm::to_string(toNDC) << std::endl; +// std::cout << "Projection2: " << glm::to_string(Projection2) << std::endl; + + return Projection2; } -//! [0] -void GeometryEngine::initCubeGeometry() +void OpenGLViewWidget::paintGL() { - // For cube we would need only 8 vertices but we have to - // duplicate vertex for each face because texture coordinate - // is different. - VertexData vertices[] = { - // Vertex data for face 0 - {QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(0.0f, 0.0f)}, // v0 - {QVector3D( 1.0f, -1.0f, 1.0f), QVector2D(0.33f, 0.0f)}, // v1 - {QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(0.0f, 0.5f)}, // v2 - {QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v3 - - // Vertex data for face 1 - {QVector3D( 1.0f, -1.0f, 1.0f), QVector2D( 0.0f, 0.5f)}, // v4 - {QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.33f, 0.5f)}, // v5 - {QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.0f, 1.0f)}, // v6 - {QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.33f, 1.0f)}, // v7 - - // Vertex data for face 2 - {QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.5f)}, // v8 - {QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(1.0f, 0.5f)}, // v9 - {QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.66f, 1.0f)}, // v10 - {QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(1.0f, 1.0f)}, // v11 - - // Vertex data for face 3 - {QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.0f)}, // v12 - {QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(1.0f, 0.0f)}, // v13 - {QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(0.66f, 0.5f)}, // v14 - {QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(1.0f, 0.5f)}, // v15 - - // Vertex data for face 4 - {QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(0.33f, 0.0f)}, // v16 - {QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.0f)}, // v17 - {QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v18 - {QVector3D( 1.0f, -1.0f, 1.0f), QVector2D(0.66f, 0.5f)}, // v19 - - // Vertex data for face 5 - {QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v20 - {QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.66f, 0.5f)}, // v21 - {QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(0.33f, 1.0f)}, // v22 - {QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.66f, 1.0f)} // v23 - }; - - // Indices for drawing cube faces using triangle strips. - // Triangle strips can be connected by duplicating indices - // between the strips. If connecting strips have opposite - // vertex order then last index of the first strip and first - // index of the second strip needs to be duplicated. If - // connecting strips have same vertex order then only last - // index of the first strip needs to be duplicated. - GLushort indices[] = { - 0, 1, 2, 3, 3, // Face 0 - triangle strip ( v0, v1, v2, v3) - 4, 4, 5, 6, 7, 7, // Face 1 - triangle strip ( v4, v5, v6, v7) - 8, 8, 9, 10, 11, 11, // Face 2 - triangle strip ( v8, v9, v10, v11) - 12, 12, 13, 14, 15, 15, // Face 3 - triangle strip (v12, v13, v14, v15) - 16, 16, 17, 18, 19, 19, // Face 4 - triangle strip (v16, v17, v18, v19) - 20, 20, 21, 22, 23 // Face 5 - triangle strip (v20, v21, v22, v23) - }; +// qDebug() << "Function Name: " << Q_FUNC_INFO; +// QElapsedTimer timer; +// timer.start(); -//! [1] - // Transfer vertex data to VBO 0 - arrayBuf.bind(); - arrayBuf.allocate(vertices, 24 * sizeof(VertexData)); + // nothing loaded yet + if(m_texture_Ydata == NULL || m_texture_Udata == NULL || m_texture_Vdata == NULL) return; +// if(m_texture_red_data == NULL ) return; - // Transfer index data to VBO 1 - indexBuf.bind(); - indexBuf.allocate(indices, 34 * sizeof(GLushort)); -//! [1] + QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); + m_program->bind(); + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glEnable(GL_DEPTH_TEST); + glEnable(GL_CULL_FACE); + glFrontFace(GL_CCW); + + m_texture_Ydata->bind(0); + m_texture_Udata->bind(1); + m_texture_Vdata->bind(2); + +// m_program->setUniformValue("textureSamplerRed", 0); +// m_program->setUniformValue("textureSamplerGreen", 1); +// m_program->setUniformValue("textureSamplerBlue", 2); + + + + glUniformMatrix4fv(m_matMVP_Loc, 1, GL_FALSE, &m_MVP[0][0]); + + + m_vertice_indices_Vbo.bind(); + m_vertices_Vbo.bind(); + m_textureLuma_coordinates_Vbo.bind(); + //m_textureChroma_coordinates_Vbo.bind(); + + glDrawElements( + GL_TRIANGLE_STRIP, // mode + m_videoFrameTriangles_indices.size(), // count + GL_UNSIGNED_INT, // type + (void*)0 // element array buffer offset + ); + + m_program->release(); + +// qDebug() << "Painting took" << timer.elapsed() << "milliseconds"; +// int msSinceLastPaint = m_measureFPSTimer.restart(); +// emit msSinceLastPaintChanged(msSinceLastPaint); } -//! [2] -void GeometryEngine::drawCubeGeometry(QOpenGLShaderProgram *program) +// function to generate the vertices corresponding to the pixels of a frame. Each vertex is centered at a pixel, borders are padded. +void OpenGLViewWidget::computeFrameVertices(int frameWidth, int frameHeight) { - // Tell OpenGL which VBOs to use - arrayBuf.bind(); - indexBuf.bind(); - // Offset for position - quintptr offset = 0; + m_videoFrameTriangles_vertices.push_back(glm::vec3(-1,-1,0)); + + m_videoFrameTriangles_vertices.push_back(glm::vec3(1,1,0)); - // Tell OpenGL programmable pipeline how to locate vertex position data - int vertexLocation = program->attributeLocation("a_position"); - program->enableAttributeArray(vertexLocation); - program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 3, sizeof(VertexData)); + m_videoFrameTriangles_vertices.push_back(glm::vec3(-1,1,0)); - // Offset for texture coordinate - offset += sizeof(QVector3D); + m_videoFrameTriangles_vertices.push_back(glm::vec3(1,-1,0)); - // Tell OpenGL programmable pipeline how to locate vertex texture coordinate data - int texcoordLocation = program->attributeLocation("a_texcoord"); - program->enableAttributeArray(texcoordLocation); - program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData)); - // Draw cube geometry using indices from VBO 1 - glDrawElements(GL_TRIANGLE_STRIP, 34, GL_UNSIGNED_SHORT, nullptr); + /*for( int h = 0; h < frameHeight; h++) + { + for(int w = 0; w < frameWidth; w++) + { + + m_videoFrameTriangles_vertices.push_back(glm::vec3(w,h,0)); + } + }*/ +} + +// function to create a mesh by connecting the vertices to triangles +void OpenGLViewWidget::computeFrameMesh(int frameWidth, int frameHeight) +{ + m_videoFrameTriangles_indices.clear(); + + m_videoFrameTriangles_indices.push_back(2); + m_videoFrameTriangles_indices.push_back(0); + m_videoFrameTriangles_indices.push_back(1); + // second triangle + m_videoFrameTriangles_indices.push_back(0); + m_videoFrameTriangles_indices.push_back(3); + m_videoFrameTriangles_indices.push_back(1); + + /*for( int h = 0; h < frameHeight; h++) + { + for(int w = 0; w < frameWidth; w++) + { + //tblr: top bottom left right + int index_pixel_tl = h*(frameWidth) + w; + int index_pixel_tr = index_pixel_tl + 1; + int index_pixel_bl = (h+1)*(frameWidth) + w; + int index_pixel_br = index_pixel_bl + 1; + + // each pixel generates two triangles, specify them so orientation is counter clockwise + // first triangle + m_videoFrameTriangles_indices.push_back(index_pixel_tl); + m_videoFrameTriangles_indices.push_back(index_pixel_bl); + m_videoFrameTriangles_indices.push_back(index_pixel_tr); + // second triangle + m_videoFrameTriangles_indices.push_back(index_pixel_bl); + m_videoFrameTriangles_indices.push_back(index_pixel_br); + m_videoFrameTriangles_indices.push_back(index_pixel_tr); + } + }*/ +} + +// Function to fill Vector which will hold the texture coordinates which maps the texture (the video data) to the frame's vertices. +void OpenGLViewWidget::computeLumaTextureCoordinates(int chromaWidth, int chromaHeight) +{ + m_videoFrameDataPoints_Luma.clear(); + + for( int h = 0; h < chromaHeight; h++) + { + for(int w = 0; w < chromaWidth; w++) + { + + float x,y; + x = (w + 0.5)/chromaWidth; //center of pixel + y = (h + 0.5)/chromaHeight; //center of pixel + + // set u for the pixel + m_videoFrameDataPoints_Luma.push_back(x); + // set v for the pixel + m_videoFrameDataPoints_Luma.push_back(y); + // set u for the pixel + } + } +} + +void OpenGLViewWidget::computeChromaTextureCoordinates(int chromaframeWidth, int chromaframeHeight) +{ + m_videoFrameDataPoints_Chroma.clear(); + + for( int h = 0; h < chromaframeHeight; h++) + { + for(int i = 0; (i < m_pixelFormat.getSubsamplingVer()); i++) + { + for(int w = 0; w < chromaframeWidth; w++) + { + float x,y; + x = (w + 0.5)/chromaframeWidth; //center of pixel + y = (h + 0.5)/chromaframeHeight; //center of pixel + + //Load extra coordinates dependent on subsampling + for(int j = 0; (j < m_pixelFormat.getSubsamplingHor()); j++) + { + // set x for the pixel + m_videoFrameDataPoints_Chroma.push_back(x); + // set y for the pixel + m_videoFrameDataPoints_Chroma.push_back(y); + } + } + } + } } -//! [2] diff --git a/YUViewLib/src/ui/views/openGLViewWidget.h b/YUViewLib/src/ui/views/openGLViewWidget.h index 48a146c4c..8827bae80 100644 --- a/YUViewLib/src/ui/views/openGLViewWidget.h +++ b/YUViewLib/src/ui/views/openGLViewWidget.h @@ -37,8 +37,7 @@ #include #include #include - - +#include #include #include #include @@ -49,20 +48,11 @@ #include #include -class GeometryEngine : protected QOpenGLFunctions -{ -public: - GeometryEngine(); - virtual ~GeometryEngine(); +// Include GLM +#include - void drawCubeGeometry(QOpenGLShaderProgram *program); +#include