From db338bc9fca641e4c98f623ccc8d3ebc1955f354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20S=C3=B8rvig?= <morten.sorvig@qt.io> Date: Tue, 17 Sep 2024 13:42:16 +0200 Subject: [PATCH] Add hellogl Simple OpenGL ES 3 example which also prints the OpenGL version --- CMakeLists.txt | 1 + deploy.sh | 1 + hellogl/CMakeLists.txt | 51 +++++++++ hellogl/glwindow.cpp | 229 +++++++++++++++++++++++++++++++++++++++++ hellogl/glwindow.h | 61 +++++++++++ hellogl/hellogles3.qrc | 5 + hellogl/logo.cpp | 103 ++++++++++++++++++ hellogl/logo.h | 28 +++++ hellogl/main.cpp | 46 +++++++++ hellogl/qtlogo.png | Bin 0 -> 2318 bytes 10 files changed, 525 insertions(+) create mode 100644 hellogl/CMakeLists.txt create mode 100644 hellogl/glwindow.cpp create mode 100644 hellogl/glwindow.h create mode 100644 hellogl/hellogles3.qrc create mode 100644 hellogl/logo.cpp create mode 100644 hellogl/logo.h create mode 100644 hellogl/main.cpp create mode 100644 hellogl/qtlogo.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 811d85c..7fce33b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,4 +3,5 @@ add_subdirectory(ordering-system) add_subdirectory(gallery_controls2) add_subdirectory(rasterwindow) add_subdirectory(principledmaterial) +add_subdirectory(hellogl) # (slate is built separately) \ No newline at end of file diff --git a/deploy.sh b/deploy.sh index cc56c84..a02531a 100755 --- a/deploy.sh +++ b/deploy.sh @@ -33,6 +33,7 @@ deploy "mandelbrot" deploy "multicanvas" deploy "rasterwindow" deploy "principledmaterial" +deploy "hellogl" # slate rm -rf slate diff --git a/hellogl/CMakeLists.txt b/hellogl/CMakeLists.txt new file mode 100644 index 0000000..7a9829f --- /dev/null +++ b/hellogl/CMakeLists.txt @@ -0,0 +1,51 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(hellogl LANGUAGES CXX) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui OpenGL) + +qt_standard_project_setup() + +qt_add_executable(hellogl + logo.cpp logo.h + glwindow.cpp glwindow.h + main.cpp +) + +set_target_properties(hellogl PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +target_link_libraries(hellogl PRIVATE + Qt6::Core + Qt6::Gui + Qt6::OpenGL +) + +# Resources: +set(hellogl_resource_files + "qtlogo.png" +) + +qt_add_resources(hellogl "hellogl" + PREFIX + "/" + FILES + ${hellogl_resource_files} +) + +install(TARGETS hellogl + BUNDLE DESTINATION . + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + +qt_generate_deploy_app_script( + TARGET hellogl + OUTPUT_SCRIPT deploy_script + NO_UNSUPPORTED_PLATFORM_ERROR +) +install(SCRIPT ${deploy_script}) diff --git a/hellogl/glwindow.cpp b/hellogl/glwindow.cpp new file mode 100644 index 0000000..8f19bd3 --- /dev/null +++ b/hellogl/glwindow.cpp @@ -0,0 +1,229 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "glwindow.h" +#include <QImage> +#include <QOpenGLTexture> +#include <QOpenGLShaderProgram> +#include <QOpenGLBuffer> +#include <QOpenGLContext> +#include <QOpenGLVertexArrayObject> +#include <QOpenGLExtraFunctions> +#include <QPropertyAnimation> +#include <QSequentialAnimationGroup> +#include <QTimer> +#include <QPainter> + +GLWindow::GLWindow() +{ + m_world.setToIdentity(); + m_world.translate(0, 0, -1); + m_world.rotate(180, 1, 0, 0); + + QSequentialAnimationGroup *animGroup = new QSequentialAnimationGroup(this); + animGroup->setLoopCount(-1); + QPropertyAnimation *zAnim0 = new QPropertyAnimation(this, QByteArrayLiteral("z")); + zAnim0->setStartValue(1.5f); + zAnim0->setEndValue(10.0f); + zAnim0->setDuration(2000); + animGroup->addAnimation(zAnim0); + QPropertyAnimation *zAnim1 = new QPropertyAnimation(this, QByteArrayLiteral("z")); + zAnim1->setStartValue(10.0f); + zAnim1->setEndValue(50.0f); + zAnim1->setDuration(4000); + zAnim1->setEasingCurve(QEasingCurve::OutElastic); + animGroup->addAnimation(zAnim1); + QPropertyAnimation *zAnim2 = new QPropertyAnimation(this, QByteArrayLiteral("z")); + zAnim2->setStartValue(50.0f); + zAnim2->setEndValue(1.5f); + zAnim2->setDuration(2000); + animGroup->addAnimation(zAnim2); + animGroup->start(); + + QPropertyAnimation* rAnim = new QPropertyAnimation(this, QByteArrayLiteral("r")); + rAnim->setStartValue(0.0f); + rAnim->setEndValue(360.0f); + rAnim->setDuration(2000); + rAnim->setLoopCount(-1); + rAnim->start(); + + QTimer::singleShot(4000, this, &GLWindow::startSecondStage); +} + +GLWindow::~GLWindow() +{ + makeCurrent(); + delete m_texture; + delete m_program; + delete m_vbo; + delete m_vao; +} + +void GLWindow::startSecondStage() +{ + QPropertyAnimation* r2Anim = new QPropertyAnimation(this, QByteArrayLiteral("r2")); + r2Anim->setStartValue(0.0f); + r2Anim->setEndValue(360.0f); + r2Anim->setDuration(20000); + r2Anim->setLoopCount(-1); + r2Anim->start(); +} + +void GLWindow::setZ(float v) +{ + m_eye.setZ(v); + m_uniformsDirty = true; + update(); +} + +void GLWindow::setR(float v) +{ + m_r = v; + m_uniformsDirty = true; + update(); +} + +void GLWindow::setR2(float v) +{ + m_r2 = v; + m_uniformsDirty = true; + update(); +} + +static const char *vertexShaderSource = + "layout(location = 0) in vec4 vertex;\n" + "layout(location = 1) in vec3 normal;\n" + "out vec3 vert;\n" + "out vec3 vertNormal;\n" + "out vec3 color;\n" + "uniform mat4 projMatrix;\n" + "uniform mat4 camMatrix;\n" + "uniform mat4 worldMatrix;\n" + "uniform mat4 myMatrix;\n" + "uniform sampler2D sampler;\n" + "void main() {\n" + " ivec2 pos = ivec2(gl_InstanceID % 32, gl_InstanceID / 32);\n" + " vec2 t = vec2(float(-16 + pos.x) * 0.8, float(-18 + pos.y) * 0.6);\n" + " float val = 2.0 * length(texelFetch(sampler, pos, 0).rgb);\n" + " mat4 wm = myMatrix * mat4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, t.x, t.y, val, 1) * worldMatrix;\n" + " color = texelFetch(sampler, pos, 0).rgb * vec3(0.4, 1.0, 0.0);\n" + " vert = vec3(wm * vertex);\n" + " vertNormal = mat3(transpose(inverse(wm))) * normal;\n" + " gl_Position = projMatrix * camMatrix * wm * vertex;\n" + "}\n"; + +static const char *fragmentShaderSource = + "in highp vec3 vert;\n" + "in highp vec3 vertNormal;\n" + "in highp vec3 color;\n" + "out highp vec4 fragColor;\n" + "uniform highp vec3 lightPos;\n" + "void main() {\n" + " highp vec3 L = normalize(lightPos - vert);\n" + " highp float NL = max(dot(normalize(vertNormal), L), 0.0);\n" + " highp vec3 col = clamp(color * 0.2 + color * 0.8 * NL, 0.0, 1.0);\n" + " fragColor = vec4(col, 1.0);\n" + "}\n"; + +QByteArray versionedShaderCode(const char *src) +{ + QByteArray versionedSrc; + + if (QOpenGLContext::currentContext()->isOpenGLES()) + versionedSrc.append(QByteArrayLiteral("#version 300 es\n")); + else + versionedSrc.append(QByteArrayLiteral("#version 330\n")); + + versionedSrc.append(src); + return versionedSrc; +} + +void GLWindow::initializeGL() +{ + QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions(); + + QSurfaceFormat format = QOpenGLContext::currentContext()->format(); + qDebug() << QString("OpenGL Version:%1.%2").arg(format.majorVersion()).arg(format.minorVersion()); + + QImage img(":/qtlogo.png"); + Q_ASSERT(!img.isNull()); + delete m_texture; + m_texture = new QOpenGLTexture(img.scaled(32, 36).mirrored()); + + delete m_program; + m_program = new QOpenGLShaderProgram; + // Prepend the correct version directive to the sources. The rest is the + // same, thanks to the common GLSL syntax. + m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, versionedShaderCode(vertexShaderSource)); + m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, versionedShaderCode(fragmentShaderSource)); + m_program->link(); + + m_projMatrixLoc = m_program->uniformLocation("projMatrix"); + m_camMatrixLoc = m_program->uniformLocation("camMatrix"); + m_worldMatrixLoc = m_program->uniformLocation("worldMatrix"); + m_myMatrixLoc = m_program->uniformLocation("myMatrix"); + m_lightPosLoc = m_program->uniformLocation("lightPos"); + + // Create a VAO. Not strictly required for ES 3, but it is for plain OpenGL. + delete m_vao; + m_vao = new QOpenGLVertexArrayObject; + if (m_vao->create()) + m_vao->bind(); + + m_program->bind(); + delete m_vbo; + m_vbo = new QOpenGLBuffer; + m_vbo->create(); + m_vbo->bind(); + m_vbo->allocate(m_logo.constData(), m_logo.count() * sizeof(GLfloat)); + f->glEnableVertexAttribArray(0); + f->glEnableVertexAttribArray(1); + f->glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), + nullptr); + f->glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), + reinterpret_cast<void *>(3 * sizeof(GLfloat))); + m_vbo->release(); + + f->glEnable(GL_DEPTH_TEST); + f->glEnable(GL_CULL_FACE); +} + +void GLWindow::resizeGL(int w, int h) +{ + m_proj.setToIdentity(); + m_proj.perspective(45.0f, GLfloat(w) / h, 0.01f, 100.0f); + m_uniformsDirty = true; +} + +void GLWindow::paintGL() +{ + // Now use QOpenGLExtraFunctions instead of QOpenGLFunctions as we want to + // do more than what GL(ES) 2.0 offers. + QOpenGLExtraFunctions *f = QOpenGLContext::currentContext()->extraFunctions(); + + f->glClearColor(0, 0, 0, 1); + f->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + m_program->bind(); + m_texture->bind(); + + if (m_uniformsDirty) { + m_uniformsDirty = false; + QMatrix4x4 camera; + camera.lookAt(m_eye, m_eye + m_target, QVector3D(0, 1, 0)); + m_program->setUniformValue(m_projMatrixLoc, m_proj); + m_program->setUniformValue(m_camMatrixLoc, camera); + QMatrix4x4 wm = m_world; + wm.rotate(m_r, 1, 1, 0); + m_program->setUniformValue(m_worldMatrixLoc, wm); + QMatrix4x4 mm; + mm.setToIdentity(); + mm.rotate(-m_r2, 1, 0, 0); + m_program->setUniformValue(m_myMatrixLoc, mm); + m_program->setUniformValue(m_lightPosLoc, QVector3D(0, 0, 70)); + } + + // Now call a function introduced in OpenGL 3.1 / OpenGL ES 3.0. We + // requested a 3.3 or ES 3.0 context, so we know this will work. + f->glDrawArraysInstanced(GL_TRIANGLES, 0, m_logo.vertexCount(), 32 * 36); +} diff --git a/hellogl/glwindow.h b/hellogl/glwindow.h new file mode 100644 index 0000000..95c6a9b --- /dev/null +++ b/hellogl/glwindow.h @@ -0,0 +1,61 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef GLWIDGET_H +#define GLWIDGET_H + +#include <QOpenGLWindow> +#include <QMatrix4x4> +#include <QVector3D> +#include "logo.h" + +QT_FORWARD_DECLARE_CLASS(QOpenGLTexture) +QT_FORWARD_DECLARE_CLASS(QOpenGLShaderProgram) +QT_FORWARD_DECLARE_CLASS(QOpenGLBuffer) +QT_FORWARD_DECLARE_CLASS(QOpenGLVertexArrayObject) + +class GLWindow : public QOpenGLWindow +{ + Q_OBJECT + Q_PROPERTY(float z READ z WRITE setZ) + Q_PROPERTY(float r READ r WRITE setR) + Q_PROPERTY(float r2 READ r2 WRITE setR2) + +public: + GLWindow(); + ~GLWindow(); + + void initializeGL(); + void resizeGL(int w, int h); + void paintGL(); + + float z() const { return m_eye.z(); } + void setZ(float v); + + float r() const { return m_r; } + void setR(float v); + float r2() const { return m_r2; } + void setR2(float v); +private slots: + void startSecondStage(); +private: + QOpenGLTexture *m_texture = nullptr; + QOpenGLShaderProgram *m_program = nullptr; + QOpenGLBuffer *m_vbo = nullptr; + QOpenGLVertexArrayObject *m_vao = nullptr; + Logo m_logo; + int m_projMatrixLoc = 0; + int m_camMatrixLoc = 0; + int m_worldMatrixLoc = 0; + int m_myMatrixLoc = 0; + int m_lightPosLoc = 0; + QMatrix4x4 m_proj; + QMatrix4x4 m_world; + QVector3D m_eye; + QVector3D m_target = {0, 0, -1}; + bool m_uniformsDirty = true; + float m_r = 0; + float m_r2 = 0; +}; + +#endif diff --git a/hellogl/hellogles3.qrc b/hellogl/hellogles3.qrc new file mode 100644 index 0000000..f3a0978 --- /dev/null +++ b/hellogl/hellogles3.qrc @@ -0,0 +1,5 @@ +<RCC> + <qresource> + <file>qtlogo.png</file> + </qresource> +</RCC> diff --git a/hellogl/logo.cpp b/hellogl/logo.cpp new file mode 100644 index 0000000..b924ba0 --- /dev/null +++ b/hellogl/logo.cpp @@ -0,0 +1,103 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "logo.h" +#include <qmath.h> + +Logo::Logo() +{ + m_data.resize(2500 * 6); + + const GLfloat x1 = +0.06f; + const GLfloat y1 = -0.14f; + const GLfloat x2 = +0.14f; + const GLfloat y2 = -0.06f; + const GLfloat x3 = +0.08f; + const GLfloat y3 = +0.00f; + const GLfloat x4 = +0.30f; + const GLfloat y4 = +0.22f; + + quad(x1, y1, x2, y2, y2, x2, y1, x1); + quad(x3, y3, x4, y4, y4, x4, y3, x3); + + extrude(x1, y1, x2, y2); + extrude(x2, y2, y2, x2); + extrude(y2, x2, y1, x1); + extrude(y1, x1, x1, y1); + extrude(x3, y3, x4, y4); + extrude(x4, y4, y4, x4); + extrude(y4, x4, y3, x3); + + const int NumSectors = 100; + + for (int i = 0; i < NumSectors; ++i) { + GLfloat angle = (i * 2 * M_PI) / NumSectors; + GLfloat angleSin = qSin(angle); + GLfloat angleCos = qCos(angle); + const GLfloat x5 = 0.30f * angleSin; + const GLfloat y5 = 0.30f * angleCos; + const GLfloat x6 = 0.20f * angleSin; + const GLfloat y6 = 0.20f * angleCos; + + angle = ((i + 1) * 2 * M_PI) / NumSectors; + angleSin = qSin(angle); + angleCos = qCos(angle); + const GLfloat x7 = 0.20f * angleSin; + const GLfloat y7 = 0.20f * angleCos; + const GLfloat x8 = 0.30f * angleSin; + const GLfloat y8 = 0.30f * angleCos; + + quad(x5, y5, x6, y6, x7, y7, x8, y8); + + extrude(x6, y6, x7, y7); + extrude(x8, y8, x5, y5); + } +} + +void Logo::add(const QVector3D &v, const QVector3D &n) +{ + GLfloat *p = m_data.data() + m_count; + *p++ = v.x(); + *p++ = v.y(); + *p++ = v.z(); + *p++ = n.x(); + *p++ = n.y(); + *p++ = n.z(); + m_count += 6; +} + +void Logo::quad(GLfloat x1, GLfloat y1, GLfloat x2, GLfloat y2, GLfloat x3, GLfloat y3, GLfloat x4, GLfloat y4) +{ + QVector3D n = QVector3D::normal(QVector3D(x4 - x1, y4 - y1, 0.0f), QVector3D(x2 - x1, y2 - y1, 0.0f)); + + add(QVector3D(x1, y1, -0.05f), n); + add(QVector3D(x4, y4, -0.05f), n); + add(QVector3D(x2, y2, -0.05f), n); + + add(QVector3D(x3, y3, -0.05f), n); + add(QVector3D(x2, y2, -0.05f), n); + add(QVector3D(x4, y4, -0.05f), n); + + n = QVector3D::normal(QVector3D(x1 - x4, y1 - y4, 0.0f), QVector3D(x2 - x4, y2 - y4, 0.0f)); + + add(QVector3D(x4, y4, 0.05f), n); + add(QVector3D(x1, y1, 0.05f), n); + add(QVector3D(x2, y2, 0.05f), n); + + add(QVector3D(x2, y2, 0.05f), n); + add(QVector3D(x3, y3, 0.05f), n); + add(QVector3D(x4, y4, 0.05f), n); +} + +void Logo::extrude(GLfloat x1, GLfloat y1, GLfloat x2, GLfloat y2) +{ + QVector3D n = QVector3D::normal(QVector3D(0.0f, 0.0f, -0.1f), QVector3D(x2 - x1, y2 - y1, 0.0f)); + + add(QVector3D(x1, y1, +0.05f), n); + add(QVector3D(x1, y1, -0.05f), n); + add(QVector3D(x2, y2, +0.05f), n); + + add(QVector3D(x2, y2, -0.05f), n); + add(QVector3D(x2, y2, +0.05f), n); + add(QVector3D(x1, y1, -0.05f), n); +} diff --git a/hellogl/logo.h b/hellogl/logo.h new file mode 100644 index 0000000..0eed3f6 --- /dev/null +++ b/hellogl/logo.h @@ -0,0 +1,28 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef LOGO_H +#define LOGO_H + +#include <qopengl.h> +#include <QList> +#include <QVector3D> + +class Logo +{ +public: + Logo(); + const GLfloat *constData() const { return m_data.constData(); } + int count() const { return m_count; } + int vertexCount() const { return m_count / 6; } + +private: + void quad(GLfloat x1, GLfloat y1, GLfloat x2, GLfloat y2, GLfloat x3, GLfloat y3, GLfloat x4, GLfloat y4); + void extrude(GLfloat x1, GLfloat y1, GLfloat x2, GLfloat y2); + void add(const QVector3D &v, const QVector3D &n); + + QList<GLfloat> m_data; + int m_count = 0; +}; + +#endif // LOGO_H diff --git a/hellogl/main.cpp b/hellogl/main.cpp new file mode 100644 index 0000000..b75702e --- /dev/null +++ b/hellogl/main.cpp @@ -0,0 +1,46 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include <QGuiApplication> +#include <QSurfaceFormat> +#include <QOpenGLContext> + +#include "glwindow.h" + +// This example demonstrates easy, cross-platform usage of OpenGL ES 3.0 functions via +// QOpenGLExtraFunctions in an application that works identically on desktop platforms +// with OpenGL 3.3 and mobile/embedded devices with OpenGL ES 3.0. + +// The code is always the same, with the exception of two places: (1) the OpenGL context +// creation has to have a sufficiently high version number for the features that are in +// use, and (2) the shader code's version directive is different. + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QSurfaceFormat fmt; + fmt.setDepthBufferSize(24); + + /* + Use the default WebGL version. Should be + OpenGL ES 3.0 / WebGL 2 on recent Qt versions. + + // Request OpenGL 3.3 core or OpenGL ES 3.0. + if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) { + qDebug("Requesting 3.3 core context"); + fmt.setVersion(3, 3); + fmt.setProfile(QSurfaceFormat::CoreProfile); + } else { + qDebug("Requesting 3.0 context"); + fmt.setVersion(3, 0); + } +*/ + + QSurfaceFormat::setDefaultFormat(fmt); + + GLWindow glWindow; + glWindow.show(); + + return app.exec(); +} diff --git a/hellogl/qtlogo.png b/hellogl/qtlogo.png new file mode 100644 index 0000000000000000000000000000000000000000..9cb2e01d3895ed7d1a81e91ed5393b474d041671 GIT binary patch literal 2318 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 E0+|Y_$p8QV literal 0 HcmV?d00001 -- GitLab