From f8de71d449a7702a8076d6b36d7e4c01fa51e7c6 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Thu, 4 Sep 2014 07:53:59 +0200 Subject: [PATCH 01/20] OpenGL rendering in python. --- docs/index.rst | 61 +++++++++++++++++ src/pyglarea.cpp | 132 +++++++++++++++++++++++++++++++++++++ src/pyglarea.h | 77 ++++++++++++++++++++++ src/pyglrenderer.cpp | 119 +++++++++++++++++++++++++++++++++ src/pyglrenderer.h | 54 +++++++++++++++ src/pyotherside_plugin.cpp | 2 + src/pyotherside_plugin.h | 1 + src/src.pro | 4 ++ 8 files changed, 450 insertions(+) create mode 100644 src/pyglarea.cpp create mode 100644 src/pyglarea.h create mode 100644 src/pyglrenderer.cpp create mode 100644 src/pyglrenderer.h diff --git a/docs/index.rst b/docs/index.rst index d448cdc..2683a23 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -864,6 +864,67 @@ This module can now be imported in QML and used as ``source`` in the QML } } +OpenGL rendering in Python +========================== + +You can render directly to Qt's OpenGL context in your Python code (i.e. via +PyOpenGL or vispy.gloo) using a ``PyGLArea`` item and supplying ``paintGL`` +and optional ``initGL`` and ``cleanupGL`` functions. The ``before`` property +controls whether to render before or after the QML scene is rendered. + +**rendergl.qml** + +.. code-block:: javascript + + import QtQuick 2.0 + import io.thp.pyotherside 1.3 + + Item { + width: 800 + height: 600 + + PyGLArea { + id: glArea + anchors.fill: parent + before: true + } + + Text { + x: 100 + y: 100 + text: "Hello World!" + } + + Python { + Component.onCompleted: { + importModule('myrenderer', function () { + glArea.initGL = 'myrenderer.initGL'; + glArea.paintGL = 'myrenderer.paintGL'; + glArea.cleanupGL = 'myrenderer.cleanupGL'; + }); + } + } + +**myrenderer.py** + +.. code-block:: python + + def initGL(): + """Initialize OpenGL stuff like textures, FBOs etc...""" + + def paintGL(x, y, width, height): + """ + Render to the OpenGL context. + + (x, y, width, height) indicates the area to render on. + + The coordinate system's origin is at the bottom left corner of the + window. + """ + + def cleanupGL(): + """Clean up OpenGL stuff initialized in initGL().""" + Building PyOtherSide ==================== diff --git a/src/pyglarea.cpp b/src/pyglarea.cpp new file mode 100644 index 0000000..d658eaf --- /dev/null +++ b/src/pyglarea.cpp @@ -0,0 +1,132 @@ + +/** + * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 + * Copyright (c) 2014, Dennis Tomas + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + **/ + +#include "qpython_priv.h" +#include "pyglarea.h" + +#include +#include +#include +#include +#include + + +PyGLArea::PyGLArea() + : m_t(0) + , m_before(false) + , m_renderer(0) +{ + connect(this, SIGNAL(windowChanged(QQuickWindow*)), this, SLOT(handleWindowChanged(QQuickWindow*))); +} + +PyGLArea::~PyGLArea() +{ + delete m_renderer; +} + +void PyGLArea::setInitGL(QString initGL) +{ + if (initGL == m_initGL) + return; + m_renderer->setInitGL(initGL); + m_initGL = initGL; + if (window()) + window()->update(); +} + +void PyGLArea::setPaintGL(QString paintGL) +{ + if (paintGL == m_paintGL) + return; + m_paintGL = paintGL; + m_renderer->setPaintGL(paintGL); + if (window()) + window()->update(); +} + +void PyGLArea::setCleanupGL(QString cleanupGL) +{ + if (cleanupGL == m_cleanupGL) + return; + m_cleanupGL = cleanupGL; + m_renderer->setCleanupGL(cleanupGL); + if (window()) + window()->update(); +} + +void PyGLArea::setBefore(bool before) +{ + if (before == m_before) + return; + m_before = before; +} + +void PyGLArea::setT(qreal t) +{ + if (t == m_t) + return; + m_t = t; + emit tChanged(); + if (window()) + window()->update(); +} + +void PyGLArea::handleWindowChanged(QQuickWindow *win) +{ + if (win) { + connect(win, SIGNAL(beforeSynchronizing()), this, SLOT(sync()), Qt::DirectConnection); + connect(win, SIGNAL(sceneGraphInvalidated()), this, SLOT(cleanup()), Qt::DirectConnection); + // If we allow QML to do the clearing, they would clear what we paint + // and nothing would show. + win->setClearBeforeRendering(false); + } +} + +void PyGLArea::update() { + window()->update(); +} + +void PyGLArea::sync() +{ + if (!m_renderer) { + m_renderer = new PyGLRenderer(); + if (m_before) + connect(window(), SIGNAL(beforeRendering()), this, SLOT(paint()), Qt::DirectConnection); + else + connect(window(), SIGNAL(afterRendering()), this, SLOT(paint()), Qt::DirectConnection); + } +} + +void PyGLArea::paint() +{ + QPointF pos = mapToScene(QPointF(.0, .0)); + m_renderer->setRect( + QRect( + (long)pos.x(), (long)(window()->height() - this->height() - pos.y()), + (long)this->width(), (long)this->height() + ) + ); + m_renderer->init(); + m_renderer->paint(); + window()->resetOpenGLState(); +} + +void PyGLArea::cleanup() +{ + m_renderer->cleanup(); +} diff --git a/src/pyglarea.h b/src/pyglarea.h new file mode 100644 index 0000000..ee0bcc9 --- /dev/null +++ b/src/pyglarea.h @@ -0,0 +1,77 @@ + +/** + * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 + * Copyright (c) 2014, Dennis Tomas + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + **/ + +#ifndef PYOTHERSIDE_PYGLAREA_H +#define PYOTHERSIDE_PYGLAREA_H + +#include "Python.h" + +#include +#include + +#include "pyglrenderer.h" + + +class PyGLArea : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(QString initGL READ initGL WRITE setInitGL) + Q_PROPERTY(QString paintGL READ paintGL WRITE setPaintGL) + Q_PROPERTY(QString cleanupGL READ cleanupGL WRITE setCleanupGL) + Q_PROPERTY(bool before READ before WRITE setBefore) + Q_PROPERTY(qreal t READ t WRITE setT NOTIFY tChanged) + +public: + PyGLArea(); + ~PyGLArea(); + + QString initGL() const { return m_initGL; }; + QString paintGL() const { return m_paintGL; }; + QString cleanupGL() const { return m_paintGL; }; + bool before() { return m_before; }; + void setInitGL(QString initGL); + void setPaintGL(QString paintGL); + void setCleanupGL(QString cleanupGL); + void setBefore(bool before); + qreal t() const { return m_t; } + void setT(qreal t); + +signals: + void tChanged(); + +public slots: + void sync(); + void update(); + +private slots: + void handleWindowChanged(QQuickWindow *win); + void paint(); + void cleanup(); + +private: + qreal m_t; + PyObject *m_paintGLCallable; + bool m_initialized; + QString m_initGL; + QString m_paintGL; + QString m_cleanupGL; + bool m_before; + PyGLRenderer *m_renderer; +}; + +#endif /* PYOTHERSIDE_PYGLAREA_H */ diff --git a/src/pyglrenderer.cpp b/src/pyglrenderer.cpp new file mode 100644 index 0000000..8a4b58f --- /dev/null +++ b/src/pyglrenderer.cpp @@ -0,0 +1,119 @@ +#include "qpython_priv.h" +#include "pyglrenderer.h" + + +PyGLRenderer::PyGLRenderer() + : m_paintGLCallable(0) + , m_initialized(false) + , m_initGL("") + , m_paintGL("") +{ +} + +PyGLRenderer::~PyGLRenderer() +{ + if (m_paintGLCallable) { + QPythonPriv *priv = QPythonPriv::instance(); + priv->enter(); + Py_DECREF(m_paintGLCallable); + priv->leave(); + m_paintGLCallable = 0; + } +} + +void PyGLRenderer::setInitGL(QString initGL) +{ + if (initGL == m_initGL) + return; + m_initGL = initGL; + m_initialized = false; +} + +void PyGLRenderer::setPaintGL(QString paintGL) +{ + if (paintGL == m_paintGL) + return; + m_paintGL = paintGL; + if (m_paintGLCallable) { + QPythonPriv *priv = QPythonPriv::instance(); + priv->enter(); + Py_DECREF(m_paintGLCallable); + priv->leave(); + } +} + +void PyGLRenderer::setCleanupGL(QString cleanupGL) +{ + if (cleanupGL == m_cleanupGL) + return; + m_cleanupGL = cleanupGL; +} + +void PyGLRenderer::setRect(QRect rect) { + m_rect = rect; +} + +void PyGLRenderer::init() { + if (!m_initialized) { + if (!m_initGL.isEmpty()) { + QPythonPriv *priv = QPythonPriv::instance(); + priv->enter(); + PyObject *initGLCallable = priv->eval(m_initGL); + PyObject *args = PyTuple_New(0); + PyObject *o = PyObject_Call(initGLCallable, args, NULL); + if (o) Py_DECREF(o); + Py_DECREF(args); + Py_DECREF(initGLCallable); + priv->leave(); + } + m_initialized = true; + } +} + + +void PyGLRenderer::paint() +{ + if (!m_initialized || m_paintGL.isEmpty()) + return; + + QPythonPriv *priv = QPythonPriv::instance(); + priv->enter(); + if (!m_paintGLCallable) + m_paintGLCallable = priv->eval(m_paintGL); + + // Call the paintGL callback with arguments x, y, width, height. + // These are the boundaries in which the callback should render, + // though it may choose to ignore them and simply paint anywhere over + // (or below) the QML scene. + // (x, y) is the bottom left corner. + // (x + width, y + height) is the top right corner. + PyObject *x = PyLong_FromLong(m_rect.x()); + PyObject *y = PyLong_FromLong(m_rect.y()); + PyObject *width = PyLong_FromLong(m_rect.width()); + PyObject *height = PyLong_FromLong(m_rect.height()); + PyObject *args = PyTuple_Pack(4, x, y, width, height); + PyObject *o = PyObject_Call(m_paintGLCallable, args, NULL); + if (o) Py_DECREF(o); + Py_DECREF(x); + Py_DECREF(y); + Py_DECREF(width); + Py_DECREF(height); + Py_DECREF(args); + priv->leave(); +} + +void PyGLRenderer::cleanup() +{ + if (!m_cleanupGL.isEmpty()) { + QPythonPriv *priv = QPythonPriv::instance(); + priv->enter(); + PyObject *cleanupGLCallable = priv->eval(m_cleanupGL); + PyObject *args = PyTuple_New(0); + PyObject *o = PyObject_Call(cleanupGLCallable, args, NULL); + if (o) Py_DECREF(o); + m_initialized = true; + Py_DECREF(args); + Py_DECREF(cleanupGLCallable); + priv->leave(); + } +} diff --git a/src/pyglrenderer.h b/src/pyglrenderer.h new file mode 100644 index 0000000..75961f2 --- /dev/null +++ b/src/pyglrenderer.h @@ -0,0 +1,54 @@ + +/** + * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 + * Copyright (c) 2014, Dennis Tomas + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + **/ + +#ifndef PYOTHERSIDE_PYGLRENDERER_H +#define PYOTHERSIDE_PYGLRENDERER_H + +#include "Python.h" + +#include +#include +#include + + +class PyGLRenderer : public QObject { + Q_OBJECT + +public: + PyGLRenderer(); + ~PyGLRenderer(); + + void init(); + void paint(); + void cleanup(); + + void setInitGL(QString initGL); + void setPaintGL(QString paintGL); + void setCleanupGL(QString cleanupGL); + void setRect(QRect rect); + +private: + QRect m_rect; + PyObject *m_paintGLCallable; + bool m_initialized; + QString m_initGL; + QString m_paintGL; + QString m_cleanupGL; +}; + +#endif /* PYOTHERSIDE_PYGLRENDERER_H */ diff --git a/src/pyotherside_plugin.cpp b/src/pyotherside_plugin.cpp index a1e4eaa..ef34276 100644 --- a/src/pyotherside_plugin.cpp +++ b/src/pyotherside_plugin.cpp @@ -18,6 +18,7 @@ #include "qpython_priv.h" #include "qpython.h" +#include "pyglarea.h" #include "qpython_imageprovider.h" #include "global_libpython_loader.h" #include "pythonlib_loader.h" @@ -66,4 +67,5 @@ PyOtherSideExtensionPlugin::registerTypes(const char *uri) qmlRegisterType(uri, 1, 2, PYOTHERSIDE_QPYTHON_NAME); qmlRegisterType(uri, 1, 3, PYOTHERSIDE_QPYTHON_NAME); qmlRegisterType(uri, 1, 4, PYOTHERSIDE_QPYTHON_NAME); + qmlRegisterType(uri, 1, 5, PYOTHERSIDE_QPYGLAREA_NAME); } diff --git a/src/pyotherside_plugin.h b/src/pyotherside_plugin.h index c611f1b..01ae0fa 100644 --- a/src/pyotherside_plugin.h +++ b/src/pyotherside_plugin.h @@ -25,6 +25,7 @@ #define PYOTHERSIDE_PLUGIN_ID "io.thp.pyotherside" #define PYOTHERSIDE_IMAGEPROVIDER_ID "python" #define PYOTHERSIDE_QPYTHON_NAME "Python" +#define PYOTHERSIDE_QPYGLAREA_NAME "PyGLArea" class Q_DECL_EXPORT PyOtherSideExtensionPlugin : public QQmlExtensionPlugin { Q_OBJECT diff --git a/src/src.pro b/src/src.pro index b801e9b..3320dde 100644 --- a/src/src.pro +++ b/src/src.pro @@ -28,6 +28,10 @@ HEADERS += pyotherside_plugin.h SOURCES += qpython_imageprovider.cpp HEADERS += qpython_imageprovider.h +# PyGLArea +SOURCES += pyglarea.cpp pyglrenderer.cpp +HEADERS += pyglarea.h pyglrenderer.h + # Importer from Qt Resources RESOURCES += qrc_importer.qrc From e126e229e8ea5905ecaa75c6329fd8eea37a8195 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Thu, 4 Sep 2014 18:03:20 +0200 Subject: [PATCH 02/20] Removed some unused private members. --- src/pyglarea.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pyglarea.h b/src/pyglarea.h index ee0bcc9..d93312f 100644 --- a/src/pyglarea.h +++ b/src/pyglarea.h @@ -65,8 +65,6 @@ private slots: private: qreal m_t; - PyObject *m_paintGLCallable; - bool m_initialized; QString m_initGL; QString m_paintGL; QString m_cleanupGL; From 13d22c6e0fbd82474e9459b53ded7ad91d049270 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Thu, 4 Sep 2014 18:04:52 +0200 Subject: [PATCH 03/20] Print exceptions in renderer methods. --- src/pyglrenderer.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/pyglrenderer.cpp b/src/pyglrenderer.cpp index 8a4b58f..edc87c2 100644 --- a/src/pyglrenderer.cpp +++ b/src/pyglrenderer.cpp @@ -1,3 +1,21 @@ + +/** + * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 + * Copyright (c) 2014, Dennis Tomas + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + **/ + #include "qpython_priv.h" #include "pyglrenderer.h" @@ -61,7 +79,7 @@ void PyGLRenderer::init() { PyObject *initGLCallable = priv->eval(m_initGL); PyObject *args = PyTuple_New(0); PyObject *o = PyObject_Call(initGLCallable, args, NULL); - if (o) Py_DECREF(o); + if (o) Py_DECREF(o); else PyErr_PrintEx(0); Py_DECREF(args); Py_DECREF(initGLCallable); priv->leave(); @@ -93,7 +111,7 @@ void PyGLRenderer::paint() PyObject *height = PyLong_FromLong(m_rect.height()); PyObject *args = PyTuple_Pack(4, x, y, width, height); PyObject *o = PyObject_Call(m_paintGLCallable, args, NULL); - if (o) Py_DECREF(o); + if (o) Py_DECREF(o); else PyErr_PrintEx(0); Py_DECREF(x); Py_DECREF(y); Py_DECREF(width); @@ -110,7 +128,7 @@ void PyGLRenderer::cleanup() PyObject *cleanupGLCallable = priv->eval(m_cleanupGL); PyObject *args = PyTuple_New(0); PyObject *o = PyObject_Call(cleanupGLCallable, args, NULL); - if (o) Py_DECREF(o); + if (o) Py_DECREF(o); else PyErr_PrintEx(0); m_initialized = true; Py_DECREF(args); Py_DECREF(cleanupGLCallable); From 3e4ea6ee5fc88fddb02a179b3941a479fe2bf0ff Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Thu, 4 Sep 2014 21:28:27 +0200 Subject: [PATCH 04/20] Recreate renderer when callbacks have been redefined. --- src/pyglarea.cpp | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/pyglarea.cpp b/src/pyglarea.cpp index d658eaf..d01b873 100644 --- a/src/pyglarea.cpp +++ b/src/pyglarea.cpp @@ -43,10 +43,9 @@ void PyGLArea::setInitGL(QString initGL) { if (initGL == m_initGL) return; - m_renderer->setInitGL(initGL); m_initGL = initGL; - if (window()) - window()->update(); + delete m_renderer; + m_renderer = 0; } void PyGLArea::setPaintGL(QString paintGL) @@ -54,9 +53,8 @@ void PyGLArea::setPaintGL(QString paintGL) if (paintGL == m_paintGL) return; m_paintGL = paintGL; - m_renderer->setPaintGL(paintGL); - if (window()) - window()->update(); + delete m_renderer; + m_renderer = 0; } void PyGLArea::setCleanupGL(QString cleanupGL) @@ -64,9 +62,8 @@ void PyGLArea::setCleanupGL(QString cleanupGL) if (cleanupGL == m_cleanupGL) return; m_cleanupGL = cleanupGL; - m_renderer->setCleanupGL(cleanupGL); - if (window()) - window()->update(); + delete m_renderer; + m_renderer = 0; } void PyGLArea::setBefore(bool before) @@ -74,6 +71,8 @@ void PyGLArea::setBefore(bool before) if (before == m_before) return; m_before = before; + delete m_renderer; + m_renderer = 0; } void PyGLArea::setT(qreal t) @@ -82,8 +81,6 @@ void PyGLArea::setT(qreal t) return; m_t = t; emit tChanged(); - if (window()) - window()->update(); } void PyGLArea::handleWindowChanged(QQuickWindow *win) @@ -98,13 +95,19 @@ void PyGLArea::handleWindowChanged(QQuickWindow *win) } void PyGLArea::update() { - window()->update(); + if (window()) + window()->update(); } void PyGLArea::sync() { if (!m_renderer) { + disconnect(window(), SIGNAL(beforeRendering()), this, SLOT(paint())); + disconnect(window(), SIGNAL(afterRendering()), this, SLOT(paint())); m_renderer = new PyGLRenderer(); + m_renderer->setInitGL(m_initGL); + m_renderer->setPaintGL(m_paintGL); + m_renderer->setCleanupGL(m_cleanupGL); if (m_before) connect(window(), SIGNAL(beforeRendering()), this, SLOT(paint()), Qt::DirectConnection); else From 52e75e918f0c3c1cd52c4d0300154a1405bb5479 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Thu, 4 Sep 2014 21:31:14 +0200 Subject: [PATCH 05/20] Added null-checks for callbacks. --- src/pyglrenderer.cpp | 65 +++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/src/pyglrenderer.cpp b/src/pyglrenderer.cpp index edc87c2..3cc9ec8 100644 --- a/src/pyglrenderer.cpp +++ b/src/pyglrenderer.cpp @@ -19,6 +19,8 @@ #include "qpython_priv.h" #include "pyglrenderer.h" +#include + PyGLRenderer::PyGLRenderer() : m_paintGLCallable(0) @@ -72,20 +74,26 @@ void PyGLRenderer::setRect(QRect rect) { } void PyGLRenderer::init() { - if (!m_initialized) { - if (!m_initGL.isEmpty()) { - QPythonPriv *priv = QPythonPriv::instance(); - priv->enter(); - PyObject *initGLCallable = priv->eval(m_initGL); - PyObject *args = PyTuple_New(0); - PyObject *o = PyObject_Call(initGLCallable, args, NULL); - if (o) Py_DECREF(o); else PyErr_PrintEx(0); - Py_DECREF(args); - Py_DECREF(initGLCallable); + if (m_initialized) + return; + + if (!m_initGL.isEmpty()) { + QPythonPriv *priv = QPythonPriv::instance(); + priv->enter(); + PyObject *initGLCallable = priv->eval(m_initGL); + if (!initGLCallable) { + qWarning() << "Init callback " << m_initGL << " not defined."; priv->leave(); + return; } - m_initialized = true; + PyObject *args = PyTuple_New(0); + PyObject *o = PyObject_Call(initGLCallable, args, NULL); + if (o) Py_DECREF(o); else PyErr_PrintEx(0); + Py_DECREF(args); + Py_DECREF(initGLCallable); + priv->leave(); } + m_initialized = true; } @@ -96,8 +104,15 @@ void PyGLRenderer::paint() QPythonPriv *priv = QPythonPriv::instance(); priv->enter(); - if (!m_paintGLCallable) + + if (!m_paintGLCallable) { m_paintGLCallable = priv->eval(m_paintGL); + if (!m_paintGLCallable) { + qWarning() << "Paint callback " << m_paintGL << " not defined."; + priv->leave(); + return; + } + } // Call the paintGL callback with arguments x, y, width, height. // These are the boundaries in which the callback should render, @@ -122,16 +137,22 @@ void PyGLRenderer::paint() void PyGLRenderer::cleanup() { - if (!m_cleanupGL.isEmpty()) { - QPythonPriv *priv = QPythonPriv::instance(); - priv->enter(); - PyObject *cleanupGLCallable = priv->eval(m_cleanupGL); - PyObject *args = PyTuple_New(0); - PyObject *o = PyObject_Call(cleanupGLCallable, args, NULL); - if (o) Py_DECREF(o); else PyErr_PrintEx(0); - m_initialized = true; - Py_DECREF(args); - Py_DECREF(cleanupGLCallable); + if (m_cleanupGL.isEmpty()) + return; + + QPythonPriv *priv = QPythonPriv::instance(); + priv->enter(); + PyObject *cleanupGLCallable = priv->eval(m_cleanupGL); + if (!cleanupGLCallable) { + qWarning() << "Cleanup callback " << m_cleanupGL << " not defined."; priv->leave(); + return; } + PyObject *args = PyTuple_New(0); + PyObject *o = PyObject_Call(cleanupGLCallable, args, NULL); + if (o) Py_DECREF(o); else PyErr_PrintEx(0); + m_initialized = true; + Py_DECREF(args); + Py_DECREF(cleanupGLCallable); + priv->leave(); } From f08ab6be25364b962e8842f89957c63ddca51e65 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Sat, 6 Sep 2014 11:21:39 +0200 Subject: [PATCH 06/20] PyGLArea: Pass renderer object instead of function names. --- docs/index.rst | 38 +++++----- src/pyglarea.cpp | 61 +++++++--------- src/pyglarea.h | 21 ++---- src/pyglrenderer.cpp | 170 ++++++++++++++++++++++++++----------------- src/pyglrenderer.h | 20 ++--- 5 files changed, 167 insertions(+), 143 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 2683a23..8c4c091 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -868,9 +868,11 @@ OpenGL rendering in Python ========================== You can render directly to Qt's OpenGL context in your Python code (i.e. via -PyOpenGL or vispy.gloo) using a ``PyGLArea`` item and supplying ``paintGL`` -and optional ``initGL`` and ``cleanupGL`` functions. The ``before`` property -controls whether to render before or after the QML scene is rendered. +PyOpenGL or vispy.gloo) using a ``PyGLArea`` item and supplying a ``renderer`` +which must provide a ``render()`` method and may optionally provide ``init()`` +and ``cleanup()`` methods. +The ``before`` property controls whether to render before or after the QML +scene is rendered. **rendergl.qml** @@ -898,9 +900,9 @@ controls whether to render before or after the QML scene is rendered. Python { Component.onCompleted: { importModule('myrenderer', function () { - glArea.initGL = 'myrenderer.initGL'; - glArea.paintGL = 'myrenderer.paintGL'; - glArea.cleanupGL = 'myrenderer.cleanupGL'; + call('myrenderer.Renderer', [], function (renderer) { + glArea.renderer = renderer; + }); }); } } @@ -909,21 +911,23 @@ controls whether to render before or after the QML scene is rendered. .. code-block:: python - def initGL(): - """Initialize OpenGL stuff like textures, FBOs etc...""" + class Renderer(object): - def paintGL(x, y, width, height): - """ - Render to the OpenGL context. + def init(self): + """Initialize OpenGL stuff like textures, FBOs etc...""" - (x, y, width, height) indicates the area to render on. + def render(self, x, y, width, height): + """ + Render to the OpenGL context. - The coordinate system's origin is at the bottom left corner of the - window. - """ + (x, y, width, height) indicates the area to render on. - def cleanupGL(): - """Clean up OpenGL stuff initialized in initGL().""" + The coordinate system's origin is at the bottom left corner of the + window. + """ + + def cleanup(self): + """Clean up OpenGL stuff initialized in initGL().""" Building PyOtherSide ==================== diff --git a/src/pyglarea.cpp b/src/pyglarea.cpp index d01b873..a6b344e 100644 --- a/src/pyglarea.cpp +++ b/src/pyglarea.cpp @@ -36,34 +36,21 @@ PyGLArea::PyGLArea() PyGLArea::~PyGLArea() { - delete m_renderer; + if (m_renderer) { + delete m_renderer; + m_renderer = 0; + } } -void PyGLArea::setInitGL(QString initGL) +void PyGLArea::setRenderer(QVariant renderer) { - if (initGL == m_initGL) + if (renderer == m_pyRenderer) return; - m_initGL = initGL; - delete m_renderer; - m_renderer = 0; -} - -void PyGLArea::setPaintGL(QString paintGL) -{ - if (paintGL == m_paintGL) - return; - m_paintGL = paintGL; - delete m_renderer; - m_renderer = 0; -} - -void PyGLArea::setCleanupGL(QString cleanupGL) -{ - if (cleanupGL == m_cleanupGL) - return; - m_cleanupGL = cleanupGL; - delete m_renderer; - m_renderer = 0; + m_pyRenderer = renderer; + if (m_renderer) { + delete m_renderer; + m_renderer = 0; + } } void PyGLArea::setBefore(bool before) @@ -71,8 +58,10 @@ void PyGLArea::setBefore(bool before) if (before == m_before) return; m_before = before; - delete m_renderer; - m_renderer = 0; + if (m_renderer) { + delete m_renderer; + m_renderer = 0; + } } void PyGLArea::setT(qreal t) @@ -101,21 +90,21 @@ void PyGLArea::update() { void PyGLArea::sync() { - if (!m_renderer) { - disconnect(window(), SIGNAL(beforeRendering()), this, SLOT(paint())); - disconnect(window(), SIGNAL(afterRendering()), this, SLOT(paint())); - m_renderer = new PyGLRenderer(); - m_renderer->setInitGL(m_initGL); + if (!m_renderer && !m_pyRenderer.isNull()) { + disconnect(window(), SIGNAL(beforeRendering()), this, SLOT(render())); + disconnect(window(), SIGNAL(afterRendering()), this, SLOT(render())); + m_renderer = new PyGLRenderer(m_pyRenderer); + /*m_renderer->setInitGL(m_initGL); m_renderer->setPaintGL(m_paintGL); - m_renderer->setCleanupGL(m_cleanupGL); + m_renderer->setCleanupGL(m_cleanupGL);*/ if (m_before) - connect(window(), SIGNAL(beforeRendering()), this, SLOT(paint()), Qt::DirectConnection); + connect(window(), SIGNAL(beforeRendering()), this, SLOT(render()), Qt::DirectConnection); else - connect(window(), SIGNAL(afterRendering()), this, SLOT(paint()), Qt::DirectConnection); + connect(window(), SIGNAL(afterRendering()), this, SLOT(render()), Qt::DirectConnection); } } -void PyGLArea::paint() +void PyGLArea::render() { QPointF pos = mapToScene(QPointF(.0, .0)); m_renderer->setRect( @@ -125,7 +114,7 @@ void PyGLArea::paint() ) ); m_renderer->init(); - m_renderer->paint(); + m_renderer->render(); window()->resetOpenGLState(); } diff --git a/src/pyglarea.h b/src/pyglarea.h index d93312f..8d131cd 100644 --- a/src/pyglarea.h +++ b/src/pyglarea.h @@ -22,17 +22,18 @@ #include "Python.h" #include +#include #include +#include "pyobject_ref.h" + #include "pyglrenderer.h" class PyGLArea : public QQuickItem { Q_OBJECT - Q_PROPERTY(QString initGL READ initGL WRITE setInitGL) - Q_PROPERTY(QString paintGL READ paintGL WRITE setPaintGL) - Q_PROPERTY(QString cleanupGL READ cleanupGL WRITE setCleanupGL) + Q_PROPERTY(QVariant renderer READ renderer WRITE setRenderer) Q_PROPERTY(bool before READ before WRITE setBefore) Q_PROPERTY(qreal t READ t WRITE setT NOTIFY tChanged) @@ -40,13 +41,9 @@ public: PyGLArea(); ~PyGLArea(); - QString initGL() const { return m_initGL; }; - QString paintGL() const { return m_paintGL; }; - QString cleanupGL() const { return m_paintGL; }; + QVariant renderer() const { return m_pyRenderer; }; bool before() { return m_before; }; - void setInitGL(QString initGL); - void setPaintGL(QString paintGL); - void setCleanupGL(QString cleanupGL); + void setRenderer(QVariant renderer); void setBefore(bool before); qreal t() const { return m_t; } void setT(qreal t); @@ -60,14 +57,12 @@ public slots: private slots: void handleWindowChanged(QQuickWindow *win); - void paint(); + void render(); void cleanup(); private: qreal m_t; - QString m_initGL; - QString m_paintGL; - QString m_cleanupGL; + QVariant m_pyRenderer; bool m_before; PyGLRenderer *m_renderer; }; diff --git a/src/pyglrenderer.cpp b/src/pyglrenderer.cpp index 3cc9ec8..9035e3e 100644 --- a/src/pyglrenderer.cpp +++ b/src/pyglrenderer.cpp @@ -17,58 +17,36 @@ **/ #include "qpython_priv.h" +#include "converter.h" +#include "pyobject_ref.h" #include "pyglrenderer.h" #include +#include -PyGLRenderer::PyGLRenderer() - : m_paintGLCallable(0) +PyGLRenderer::PyGLRenderer(QVariant pyRenderer) + : m_pyRendererObject(0) + , m_renderMethod(0) , m_initialized(false) - , m_initGL("") - , m_paintGL("") { + m_pyRenderer = pyRenderer; } PyGLRenderer::~PyGLRenderer() { - if (m_paintGLCallable) { + if (m_pyRendererObject) { QPythonPriv *priv = QPythonPriv::instance(); priv->enter(); - Py_DECREF(m_paintGLCallable); + Py_CLEAR(m_pyRendererObject); + if (m_renderMethod) + Py_DECREF(m_renderMethod); priv->leave(); - m_paintGLCallable = 0; + m_pyRendererObject = 0; + m_renderMethod = 0; } } -void PyGLRenderer::setInitGL(QString initGL) -{ - if (initGL == m_initGL) - return; - m_initGL = initGL; - m_initialized = false; -} - -void PyGLRenderer::setPaintGL(QString paintGL) -{ - if (paintGL == m_paintGL) - return; - m_paintGL = paintGL; - if (m_paintGLCallable) { - QPythonPriv *priv = QPythonPriv::instance(); - priv->enter(); - Py_DECREF(m_paintGLCallable); - priv->leave(); - } -} - -void PyGLRenderer::setCleanupGL(QString cleanupGL) -{ - if (cleanupGL == m_cleanupGL) - return; - m_cleanupGL = cleanupGL; -} - void PyGLRenderer::setRect(QRect rect) { m_rect = rect; } @@ -77,41 +55,54 @@ void PyGLRenderer::init() { if (m_initialized) return; - if (!m_initGL.isEmpty()) { - QPythonPriv *priv = QPythonPriv::instance(); - priv->enter(); - PyObject *initGLCallable = priv->eval(m_initGL); - if (!initGLCallable) { - qWarning() << "Init callback " << m_initGL << " not defined."; - priv->leave(); - return; - } - PyObject *args = PyTuple_New(0); - PyObject *o = PyObject_Call(initGLCallable, args, NULL); - if (o) Py_DECREF(o); else PyErr_PrintEx(0); - Py_DECREF(args); - Py_DECREF(initGLCallable); + QPythonPriv *priv = QPythonPriv::instance(); + priv->enter(); + + PyObject *pyRendererObject = getPyRendererObject(); + if (!pyRendererObject || pyRendererObject == Py_None) { priv->leave(); + return; } + + if (!PyObject_HasAttrString(m_pyRendererObject, "init")) { + // Optional init() method not found, consider the renderer initialized. + priv->leave(); + m_initialized = true; + return; + } + + PyObject *initMethod = PyObject_GetAttrString(m_pyRendererObject, "init"); + if (!initMethod) { + qWarning() << "Failed to get init method of renderer."; + PyErr_PrintEx(0); + priv->leave(); + return; + } + + PyObject *args = PyTuple_New(0); + PyObject *o = PyObject_Call(initMethod, args, NULL); + if (o) Py_DECREF(o); else PyErr_PrintEx(0); + Py_DECREF(args); + Py_DECREF(initMethod); + priv->leave(); m_initialized = true; } -void PyGLRenderer::paint() +void PyGLRenderer::render() { - if (!m_initialized || m_paintGL.isEmpty()) + if (!m_initialized) return; QPythonPriv *priv = QPythonPriv::instance(); priv->enter(); - if (!m_paintGLCallable) { - m_paintGLCallable = priv->eval(m_paintGL); - if (!m_paintGLCallable) { - qWarning() << "Paint callback " << m_paintGL << " not defined."; - priv->leave(); - return; - } + PyObject *renderMethod = getRenderMethod(); + if (!renderMethod) { + qWarning() << "Failed to get render method of renderer."; + PyErr_PrintEx(0); + priv->leave(); + return; } // Call the paintGL callback with arguments x, y, width, height. @@ -125,7 +116,7 @@ void PyGLRenderer::paint() PyObject *width = PyLong_FromLong(m_rect.width()); PyObject *height = PyLong_FromLong(m_rect.height()); PyObject *args = PyTuple_Pack(4, x, y, width, height); - PyObject *o = PyObject_Call(m_paintGLCallable, args, NULL); + PyObject *o = PyObject_Call(renderMethod, args, NULL); if (o) Py_DECREF(o); else PyErr_PrintEx(0); Py_DECREF(x); Py_DECREF(y); @@ -137,22 +128,67 @@ void PyGLRenderer::paint() void PyGLRenderer::cleanup() { - if (m_cleanupGL.isEmpty()) - return; - QPythonPriv *priv = QPythonPriv::instance(); priv->enter(); - PyObject *cleanupGLCallable = priv->eval(m_cleanupGL); - if (!cleanupGLCallable) { - qWarning() << "Cleanup callback " << m_cleanupGL << " not defined."; + + PyObject *pyRendererObject = getPyRendererObject(); + if (!pyRendererObject || pyRendererObject == Py_None || + !PyObject_HasAttrString(m_pyRendererObject, "cleanup")) { priv->leave(); return; } + + PyObject *cleanupMethod = PyObject_GetAttrString(m_pyRendererObject, "cleanup"); + if (!cleanupMethod) { + qWarning() << "Failed to get cleanup method of renderer."; + PyErr_PrintEx(0); + priv->leave(); + return; + } + PyObject *args = PyTuple_New(0); - PyObject *o = PyObject_Call(cleanupGLCallable, args, NULL); + PyObject *o = PyObject_Call(cleanupMethod, args, NULL); if (o) Py_DECREF(o); else PyErr_PrintEx(0); m_initialized = true; Py_DECREF(args); - Py_DECREF(cleanupGLCallable); + Py_DECREF(cleanupMethod); priv->leave(); } + +PyObject *PyGLRenderer::getPyRendererObject() { + if (m_pyRendererObject) { + return m_pyRendererObject; + } + if (m_pyRenderer.userType() != qMetaTypeId()) { + qWarning() << "Renderer must be of type PyObjectRef (got " + << m_pyRenderer << ")."; + return NULL; + } + m_pyRendererObject = m_pyRenderer.value().newRef(); + return m_pyRendererObject; +} + +PyObject *PyGLRenderer::getRenderMethod() { + if (m_renderMethod) { + return m_renderMethod; + } + + PyObject *pyRendererObject = getPyRendererObject(); + if (!pyRendererObject || pyRendererObject == Py_None) { + return NULL; + } + + if (!PyObject_HasAttrString(m_pyRendererObject, "render")) { + qWarning() << "Renderer has no render method."; + return NULL; + } + + PyObject *m_renderMethod = PyObject_GetAttrString(m_pyRendererObject, "render"); + if (!m_renderMethod) { + qWarning() << "Failed to get render method of renderer."; + PyErr_PrintEx(0); + return NULL; + } + + return m_renderMethod; +} diff --git a/src/pyglrenderer.h b/src/pyglrenderer.h index 75961f2..f3d756e 100644 --- a/src/pyglrenderer.h +++ b/src/pyglrenderer.h @@ -21,34 +21,34 @@ #include "Python.h" -#include +#include #include #include +#include class PyGLRenderer : public QObject { Q_OBJECT public: - PyGLRenderer(); + PyGLRenderer(QVariant pyRenderer); ~PyGLRenderer(); void init(); - void paint(); + void render(); void cleanup(); - void setInitGL(QString initGL); - void setPaintGL(QString paintGL); - void setCleanupGL(QString cleanupGL); void setRect(QRect rect); private: QRect m_rect; - PyObject *m_paintGLCallable; + QVariant m_pyRenderer; + PyObject *m_pyRendererObject; + PyObject *m_renderMethod; bool m_initialized; - QString m_initGL; - QString m_paintGL; - QString m_cleanupGL; + + PyObject *getPyRendererObject(); + PyObject *getRenderMethod(); }; #endif /* PYOTHERSIDE_PYGLRENDERER_H */ From eef52665553e3ac2fcb913afec7cd40e8624ffeb Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Sat, 6 Sep 2014 21:38:08 +0200 Subject: [PATCH 07/20] Update PyGLArea when properties have changed. --- src/pyglarea.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pyglarea.cpp b/src/pyglarea.cpp index a6b344e..edc6534 100644 --- a/src/pyglarea.cpp +++ b/src/pyglarea.cpp @@ -51,6 +51,7 @@ void PyGLArea::setRenderer(QVariant renderer) delete m_renderer; m_renderer = 0; } + update(); } void PyGLArea::setBefore(bool before) @@ -62,6 +63,7 @@ void PyGLArea::setBefore(bool before) delete m_renderer; m_renderer = 0; } + update(); } void PyGLArea::setT(qreal t) @@ -94,9 +96,6 @@ void PyGLArea::sync() disconnect(window(), SIGNAL(beforeRendering()), this, SLOT(render())); disconnect(window(), SIGNAL(afterRendering()), this, SLOT(render())); m_renderer = new PyGLRenderer(m_pyRenderer); - /*m_renderer->setInitGL(m_initGL); - m_renderer->setPaintGL(m_paintGL); - m_renderer->setCleanupGL(m_cleanupGL);*/ if (m_before) connect(window(), SIGNAL(beforeRendering()), this, SLOT(render()), Qt::DirectConnection); else From 873d16844460268ab28b3759a9925fe479d60e6b Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Mon, 8 Sep 2014 11:09:08 +0200 Subject: [PATCH 08/20] Improved PyGLArea documentation, added example. --- docs/index.rst | 202 ++++++++++++++++++++++++++++++++++-------- examples/pyglarea.py | 75 ++++++++++++++++ examples/pyglarea.qml | 62 +++++++++++++ 3 files changed, 300 insertions(+), 39 deletions(-) create mode 100644 examples/pyglarea.py create mode 100644 examples/pyglarea.qml diff --git a/docs/index.rst b/docs/index.rst index 8c4c091..164e63e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -525,6 +525,40 @@ deleted (there's no way for PyOtherSide to prevent referenced QObjects from being deleted, but PyOtherSide tries hard to detect the deletion of objects and give meaningful error messages in case the reference is accessed). +OpenGL rendering in Python +========================== + +You can render directly to a QML application's OpenGL context in your Python +code (i.e. via PyOpenGL or vispy.gloo) by using a ``PyGLArea`` item with the +following properties: + +**before** + Whether to draw the ``PyGLArea`` before (under) or after (over) the scene. + Defaults to ``false``. + +**renderer** + Reference to a python object providing the ``IRenderer`` interface: + +.. function:: IRenderer.init() + + Initialize OpenGL resources required for rendering. + This method is optional. + +.. function:: IRenderer.render(x, y, width, height) + + Render within the given area. + It is the renderer's responsibility to confine itself to that area. + Also, it must unbind any used resources to leave the context in a clean + state. + + ``(x, y)`` is the position of the bottom left corner of the area, in + window coordinates, e.g. (0, 0) is the bottom left corner of the window. + +.. function:: IRenderer.cleanup() + + Free any resources allocated by :func:`IRenderer.init`. + This method is optional. + Cookbook ======== @@ -864,17 +898,94 @@ This module can now be imported in QML and used as ``source`` in the QML } } -OpenGL rendering in Python -========================== +Rendering with PyOpenGL +----------------------- -You can render directly to Qt's OpenGL context in your Python code (i.e. via -PyOpenGL or vispy.gloo) using a ``PyGLArea`` item and supplying a ``renderer`` -which must provide a ``render()`` method and may optionally provide ``init()`` -and ``cleanup()`` methods. -The ``before`` property controls whether to render before or after the QML -scene is rendered. +The example below shows how to do raw OpenGL rendering in PyOpenGL using a +PyGLArea. It has been adapted from the tutorial in the Qt documentation at +http://qt-project.org/doc/qt-5/qtquick-scenegraph-openglunderqml-example.html. -**rendergl.qml** +**pyglarea.py** + +.. code-block:: python + + import numpy + + from OpenGL.GL import * + from OpenGL.GL.shaders import compileShader, compileProgram + + VERTEX_SHADER = """#version 130 + attribute highp vec4 vertices; + varying highp vec2 coords; + + void main() { + gl_Position = vertices; + coords = vertices.xy; + } + """ + + FRAGMENT_SHADER = """#version 130 + uniform lowp float t; + varying highp vec2 coords; + void main() { + lowp float i = 1. - (pow(abs(coords.x), 4.) + pow(abs(coords.y), 4.)); + i = smoothstep(t - 0.8, t + 0.8, i); + i = floor(i * 20.) / 20.; + gl_FragColor = vec4(coords * .5 + .5, i, i); + } + """ + + class Renderer(object): + + def __init__(self): + self.t = 0.0 + self.values = numpy.array([ + -1.0, -1.0, + 1.0, -1.0, + -1.0, 1.0, + 1.0, 1.0 + ], dtype=numpy.float32) + + def set_t(self, t): + self.t = t + + def init(self): + self.vertexbuffer = glGenBuffers(1) + vertex_shader = compileShader(VERTEX_SHADER, GL_VERTEX_SHADER) + fragment_shader = compileShader(FRAGMENT_SHADER, GL_FRAGMENT_SHADER) + self.program = compileProgram(vertex_shader, fragment_shader) + self.vertices_attr = glGetAttribLocation(self.program, b'vertices') + self.t_attr = glGetUniformLocation(self.program, b't') + + def render(self, x, y, width, height): + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + glViewport(x, y, width, height) + glUseProgram(self.program) + try: + glDisable(GL_DEPTH_TEST) + glClearColor(0, 0, 0, 1) + glClear(GL_COLOR_BUFFER_BIT) + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE) + + glBindBuffer(GL_ARRAY_BUFFER, self.vertexbuffer) + glEnableVertexAttribArray(self.vertices_attr) + glBufferData(GL_ARRAY_BUFFER, self.values, GL_STATIC_DRAW) + glVertexAttribPointer(self.vertices_attr, 2, GL_FLOAT, GL_FALSE, 0, None) + glUniform1f(self.t_attr, self.t) + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4) + finally: + glDisableVertexAttribArray(0) + glBindBuffer(GL_ARRAY_BUFFER, 0) + glUseProgram(0) + + def cleanup(self): + glDeleteProgram(self.program) + glDeleteBuffers(1, [self.vertexbuffer]) + +**pyglarea.qml** .. code-block:: javascript @@ -882,52 +993,65 @@ scene is rendered. import io.thp.pyotherside 1.3 Item { - width: 800 - height: 600 + width: 320 + height: 480 PyGLArea { id: glArea anchors.fill: parent before: true + property var t: 0 + + SequentialAnimation on t { + NumberAnimation { to: 1; duration: 2500; easing.type: Easing.InQuad } + NumberAnimation { to: 0; duration: 2500; easing.type: Easing.OutQuad } + loops: Animation.Infinite + running: true + } + + onTChanged: { + if (renderer) { + py.callMethod(renderer, 'set_t', [t], update); + } + } + } + + Rectangle { + color: Qt.rgba(1, 1, 1, 0.7) + radius: 10 + border.width: 1 + border.color: "white" + anchors.fill: label + anchors.margins: -10 } Text { - x: 100 - y: 100 - text: "Hello World!" + id: label + color: "black" + wrapMode: Text.WordWrap + text: "The background here is a squircle rendered with raw OpenGL using a PyGLArea. This text label and its border is rendered using QML" + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.margins: 20 } Python { + id: py + Component.onCompleted: { - importModule('myrenderer', function () { - call('myrenderer.Renderer', [], function (renderer) { - glArea.renderer = renderer; + addImportPath(Qt.resolvedUrl('.')); + importModule('pyglarea', function () { + call('pyglarea.Renderer', [], function (renderer) { + glArea.renderer = renderer; + }); }); - }); + } + + onError: console.log(traceback); } } -**myrenderer.py** - -.. code-block:: python - - class Renderer(object): - - def init(self): - """Initialize OpenGL stuff like textures, FBOs etc...""" - - def render(self, x, y, width, height): - """ - Render to the OpenGL context. - - (x, y, width, height) indicates the area to render on. - - The coordinate system's origin is at the bottom left corner of the - window. - """ - - def cleanup(self): - """Clean up OpenGL stuff initialized in initGL().""" Building PyOtherSide ==================== diff --git a/examples/pyglarea.py b/examples/pyglarea.py new file mode 100644 index 0000000..cbb1379 --- /dev/null +++ b/examples/pyglarea.py @@ -0,0 +1,75 @@ +import numpy + +from OpenGL.GL import * +from OpenGL.GL.shaders import compileShader, compileProgram + +VERTEX_SHADER = """#version 130 +attribute highp vec4 vertices; +varying highp vec2 coords; + +void main() { + gl_Position = vertices; + coords = vertices.xy; +} +""" + +FRAGMENT_SHADER = """#version 130 +uniform lowp float t; +varying highp vec2 coords; +void main() { + lowp float i = 1. - (pow(abs(coords.x), 4.) + pow(abs(coords.y), 4.)); + i = smoothstep(t - 0.8, t + 0.8, i); + i = floor(i * 20.) / 20.; + gl_FragColor = vec4(coords * .5 + .5, i, i); +} +""" + +class Renderer(object): + + def __init__(self): + self.t = 0.0 + self.values = numpy.array([ + -1.0, -1.0, + 1.0, -1.0, + -1.0, 1.0, + 1.0, 1.0 + ], dtype=numpy.float32) + + def set_t(self, t): + self.t = t + + def init(self): + self.vertexbuffer = glGenBuffers(1) + vertex_shader = compileShader(VERTEX_SHADER, GL_VERTEX_SHADER) + fragment_shader = compileShader(FRAGMENT_SHADER, GL_FRAGMENT_SHADER) + self.program = compileProgram(vertex_shader, fragment_shader) + self.vertices_attr = glGetAttribLocation(self.program, b'vertices') + self.t_attr = glGetUniformLocation(self.program, b't') + + def render(self, x, y, width, height): + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + glViewport(x, y, width, height) + glUseProgram(self.program) + try: + glDisable(GL_DEPTH_TEST) + glClearColor(0, 0, 0, 1) + glClear(GL_COLOR_BUFFER_BIT) + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE) + + glBindBuffer(GL_ARRAY_BUFFER, self.vertexbuffer) + glEnableVertexAttribArray(self.vertices_attr) + glBufferData(GL_ARRAY_BUFFER, self.values, GL_STATIC_DRAW) + glVertexAttribPointer(self.vertices_attr, 2, GL_FLOAT, GL_FALSE, 0, None) + glUniform1f(self.t_attr, self.t) + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4) + finally: + glDisableVertexAttribArray(0) + glBindBuffer(GL_ARRAY_BUFFER, 0) + glUseProgram(0) + + def cleanup(self): + glDeleteProgram(self.program) + glDeleteBuffers(1, [self.vertexbuffer]) diff --git a/examples/pyglarea.qml b/examples/pyglarea.qml new file mode 100644 index 0000000..1954dd4 --- /dev/null +++ b/examples/pyglarea.qml @@ -0,0 +1,62 @@ +import QtQuick 2.0 +import io.thp.pyotherside 1.3 + +Item { + width: 320 + height: 480 + + PyGLArea { + id: glArea + anchors.fill: parent + before: true + property var t: 0 + + SequentialAnimation on t { + NumberAnimation { to: 1; duration: 2500; easing.type: Easing.InQuad } + NumberAnimation { to: 0; duration: 2500; easing.type: Easing.OutQuad } + loops: Animation.Infinite + running: true + } + + onTChanged: { + if (renderer) { + py.callMethod(renderer, 'set_t', [t], update); + } + } + } + + Rectangle { + color: Qt.rgba(1, 1, 1, 0.7) + radius: 10 + border.width: 1 + border.color: "white" + anchors.fill: label + anchors.margins: -10 + } + + Text { + id: label + color: "black" + wrapMode: Text.WordWrap + text: "The background here is a squircle rendered with raw OpenGL using a PyGLArea. This text label and its border is rendered using QML" + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.margins: 20 + } + + Python { + id: py + + Component.onCompleted: { + addImportPath(Qt.resolvedUrl('.')); + importModule('pyglarea', function () { + call('pyglarea.Renderer', [], function (renderer) { + glArea.renderer = renderer; + }); + }); + } + + onError: console.log(traceback); + } +} From 177c5203dcc77e34168296a7700b6c8989aaf3af Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Mon, 8 Sep 2014 11:12:44 +0200 Subject: [PATCH 09/20] Removed property PyGLArea.t (leftover from Qt tutorial). --- src/pyglarea.cpp | 11 +---------- src/pyglarea.h | 7 ------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/pyglarea.cpp b/src/pyglarea.cpp index edc6534..23e582e 100644 --- a/src/pyglarea.cpp +++ b/src/pyglarea.cpp @@ -27,8 +27,7 @@ PyGLArea::PyGLArea() - : m_t(0) - , m_before(false) + : m_before(false) , m_renderer(0) { connect(this, SIGNAL(windowChanged(QQuickWindow*)), this, SLOT(handleWindowChanged(QQuickWindow*))); @@ -66,14 +65,6 @@ void PyGLArea::setBefore(bool before) update(); } -void PyGLArea::setT(qreal t) -{ - if (t == m_t) - return; - m_t = t; - emit tChanged(); -} - void PyGLArea::handleWindowChanged(QQuickWindow *win) { if (win) { diff --git a/src/pyglarea.h b/src/pyglarea.h index 8d131cd..cc7d3c2 100644 --- a/src/pyglarea.h +++ b/src/pyglarea.h @@ -35,7 +35,6 @@ class PyGLArea : public QQuickItem Q_OBJECT Q_PROPERTY(QVariant renderer READ renderer WRITE setRenderer) Q_PROPERTY(bool before READ before WRITE setBefore) - Q_PROPERTY(qreal t READ t WRITE setT NOTIFY tChanged) public: PyGLArea(); @@ -45,11 +44,6 @@ public: bool before() { return m_before; }; void setRenderer(QVariant renderer); void setBefore(bool before); - qreal t() const { return m_t; } - void setT(qreal t); - -signals: - void tChanged(); public slots: void sync(); @@ -61,7 +55,6 @@ private slots: void cleanup(); private: - qreal m_t; QVariant m_pyRenderer; bool m_before; PyGLRenderer *m_renderer; From 0ab3edabc58f761e18583a2ee4c6142e098e1bf8 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Tue, 9 Sep 2014 10:04:11 +0200 Subject: [PATCH 10/20] Only disable automatic clearing when rendering before QML. --- src/pyglarea.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/pyglarea.cpp b/src/pyglarea.cpp index 23e582e..4bc408e 100644 --- a/src/pyglarea.cpp +++ b/src/pyglarea.cpp @@ -70,9 +70,6 @@ void PyGLArea::handleWindowChanged(QQuickWindow *win) if (win) { connect(win, SIGNAL(beforeSynchronizing()), this, SLOT(sync()), Qt::DirectConnection); connect(win, SIGNAL(sceneGraphInvalidated()), this, SLOT(cleanup()), Qt::DirectConnection); - // If we allow QML to do the clearing, they would clear what we paint - // and nothing would show. - win->setClearBeforeRendering(false); } } @@ -87,10 +84,15 @@ void PyGLArea::sync() disconnect(window(), SIGNAL(beforeRendering()), this, SLOT(render())); disconnect(window(), SIGNAL(afterRendering()), this, SLOT(render())); m_renderer = new PyGLRenderer(m_pyRenderer); - if (m_before) + if (m_before) { + // If we allow QML to do the clearing, they would clear what we paint + // and nothing would show. + window()->setClearBeforeRendering(false); connect(window(), SIGNAL(beforeRendering()), this, SLOT(render()), Qt::DirectConnection); - else + } else { + window()->setClearBeforeRendering(true); connect(window(), SIGNAL(afterRendering()), this, SLOT(render()), Qt::DirectConnection); + } } } From 6209cec9f21a2d8f15912127c33c166c4a6358c2 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Thu, 2 Oct 2014 10:38:35 +0200 Subject: [PATCH 11/20] OpenGL rendering to FBO. --- examples/pyfbo.py | 74 +++++++++++++++++ examples/pyfbo.qml | 160 +++++++++++++++++++++++++++++++++++++ src/pyfbo.cpp | 98 +++++++++++++++++++++++ src/pyfbo.h | 49 ++++++++++++ src/pyglrenderer.cpp | 44 +++++----- src/pyglrenderer.h | 3 +- src/pyotherside_plugin.cpp | 2 + src/pyotherside_plugin.h | 1 + src/src.pro | 4 + 9 files changed, 416 insertions(+), 19 deletions(-) create mode 100644 examples/pyfbo.py create mode 100644 examples/pyfbo.qml create mode 100644 src/pyfbo.cpp create mode 100644 src/pyfbo.h diff --git a/examples/pyfbo.py b/examples/pyfbo.py new file mode 100644 index 0000000..029aed5 --- /dev/null +++ b/examples/pyfbo.py @@ -0,0 +1,74 @@ +import numpy + +from OpenGL.GL import * +from OpenGL.GL.shaders import compileShader, compileProgram + + +VERTEX_SHADER = """#version 130 +attribute highp vec4 vertices; +varying highp vec2 coords; + +void main() { + gl_Position = vertices; + coords = vertices.xy; +} +""" + +FRAGMENT_SHADER = """#version 130 +uniform lowp float t; +varying highp vec2 coords; +void main() { + lowp float i = 1. - (pow(abs(coords.x), 4.) + pow(abs(coords.y), 4.)); + i = smoothstep(t - 0.8, t + 0.8, i); + i = floor(i * 20.) / 20.; + gl_FragColor = vec4(coords * .5 + .5, i, i); +} +""" + + +class Renderer(object): + + def __init__(self): + self.t = 0.0 + self.values = numpy.array([ + -1.0, -1.0, + 1.0, -1.0, + -1.0, 1.0, + 1.0, 1.0 + ], dtype=numpy.float32) + + def set_t(self, t): + self.t = t + + def init(self): + self.vertexbuffer = glGenBuffers(1) + vertex_shader = compileShader(VERTEX_SHADER, GL_VERTEX_SHADER) + fragment_shader = compileShader(FRAGMENT_SHADER, GL_FRAGMENT_SHADER) + self.program = compileProgram(vertex_shader, fragment_shader) + self.vertices_attr = glGetAttribLocation(self.program, b'vertices') + self.t_attr = glGetUniformLocation(self.program, b't') + + def render(self): + glUseProgram(self.program) + try: + glDisable(GL_DEPTH_TEST) + glClearColor(0, 0, 0, 1) + glClear(GL_COLOR_BUFFER_BIT) + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE) + + glBindBuffer(GL_ARRAY_BUFFER, self.vertexbuffer) + glEnableVertexAttribArray(self.vertices_attr) + glBufferData(GL_ARRAY_BUFFER, self.values, GL_STATIC_DRAW) + glVertexAttribPointer(self.vertices_attr, 2, GL_FLOAT, GL_FALSE, 0, None) + glUniform1f(self.t_attr, self.t) + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4) + finally: + glDisableVertexAttribArray(self.vertices_attr) + glBindBuffer(GL_ARRAY_BUFFER, 0) + glUseProgram(0) + + def cleanup(self): + glDeleteProgram(self.program) + glDeleteBuffers(1, [self.vertexbuffer]) diff --git a/examples/pyfbo.qml b/examples/pyfbo.qml new file mode 100644 index 0000000..5500589 --- /dev/null +++ b/examples/pyfbo.qml @@ -0,0 +1,160 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.0 +import io.thp.pyotherside 1.3 + + +Item { + width: 400 + height: 400 + + // The checkers background + ShaderEffect { + id: tileBackground + anchors.fill: parent + + property real tileSize: 16 + property color color1: Qt.rgba(0.9, 0.9, 0.9, 1); + property color color2: Qt.rgba(0.85, 0.85, 0.85, 1); + + property size pixelSize: Qt.size(width / tileSize, height / tileSize); + + fragmentShader: + " + uniform lowp vec4 color1; + uniform lowp vec4 color2; + uniform highp vec2 pixelSize; + varying highp vec2 qt_TexCoord0; + void main() { + highp vec2 tc = sign(sin(3.14152 * qt_TexCoord0 * pixelSize)); + if (tc.x != tc.y) + gl_FragColor = color1; + else + gl_FragColor = color2; + } + " + } + + PyFBO { + id: fbo + anchors.fill: parent + anchors.margins: 10 + property var t: 0 + + SequentialAnimation on t { + NumberAnimation { to: 1; duration: 2500; easing.type: Easing.InQuad } + NumberAnimation { to: 0; duration: 2500; easing.type: Easing.OutQuad } + loops: Animation.Infinite + running: true + } + + // The transform is just to show something interesting.. + transform: [ + Rotation { id: rotation; axis.x: 0; axis.z: 0; axis.y: 1; angle: 0; origin.x: fbo.width / 2; origin.y: fbo.height / 2; }, + Translate { id: txOut; x: -fbo.width / 2; y: -fbo.height / 2 }, + Scale { id: scale; }, + Translate { id: txIn; x: fbo.width / 2; y: fbo.height / 2 } + ] + + onTChanged: { + if (renderer) { + py.callMethod(renderer, 'set_t', [t], update); + } + } + } + + // Just to show something interesting + SequentialAnimation { + PauseAnimation { duration: 5000 } + ParallelAnimation { + NumberAnimation { target: scale; property: "xScale"; to: 0.6; duration: 1000; easing.type: Easing.InOutBack } + NumberAnimation { target: scale; property: "yScale"; to: 0.6; duration: 1000; easing.type: Easing.InOutBack } + } + NumberAnimation { target: rotation; property: "angle"; to: 80; duration: 1000; easing.type: Easing.InOutCubic } + NumberAnimation { target: rotation; property: "angle"; to: -80; duration: 1000; easing.type: Easing.InOutCubic } + NumberAnimation { target: rotation; property: "angle"; to: 0; duration: 1000; easing.type: Easing.InOutCubic } + NumberAnimation { target: fbo; property: "opacity"; to: 0.5; duration: 1000; easing.type: Easing.InOutCubic } + PauseAnimation { duration: 1000 } + NumberAnimation { target: fbo; property: "opacity"; to: 0.8; duration: 1000; easing.type: Easing.InOutCubic } + ParallelAnimation { + NumberAnimation { target: scale; property: "xScale"; to: 1; duration: 1000; easing.type: Easing.InOutBack } + NumberAnimation { target: scale; property: "yScale"; to: 1; duration: 1000; easing.type: Easing.InOutBack } + } + running: true + loops: Animation.Infinite + } + + Rectangle { + id: labelFrame + anchors.margins: -10 + radius: 5 + color: "white" + border.color: "black" + opacity: 0.8 + anchors.fill: label + } + + Text { + id: label + anchors.bottom: fbo.bottom + anchors.left: fbo.left + anchors.right: fbo.right + anchors.margins: 20 + wrapMode: Text.WordWrap + text: "The squircle is an FBO, rendered by the application on the scene graph rendering thread. The FBO is managed and displayed using a PyFBO item." + } + + Python { + id: py + + Component.onCompleted: { + addImportPath(Qt.resolvedUrl('.')); + importModule('pyfbo', function () { + call('pyfbo.Renderer', [], function (renderer) { + fbo.renderer = renderer; + }); + }); + } + + onError: console.log(traceback); + } + +} diff --git a/src/pyfbo.cpp b/src/pyfbo.cpp new file mode 100644 index 0000000..4e78a26 --- /dev/null +++ b/src/pyfbo.cpp @@ -0,0 +1,98 @@ + +/** + * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 + * Copyright (c) 2014, Dennis Tomas + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + **/ + +#include "pyfbo.h" + +#include + + +class PyFboRenderer : public QQuickFramebufferObject::Renderer +{ +public: + PyFboRenderer() + : m_renderer(0) + { + } + + void render() { + if (!m_renderer) { + return; + } + m_renderer->init(); + m_renderer->render(); + update(); + } + + void setRenderer(PyGLRenderer *renderer) { + m_renderer = renderer; + update(); + } + + QOpenGLFramebufferObject *createFramebufferObject(const QSize &size) { + QOpenGLFramebufferObjectFormat format; + // TODO: get the FBO format from the PyGLRenderer. + return new QOpenGLFramebufferObject(size, format); + } +private: + PyGLRenderer *m_renderer; +}; + +PyFbo::PyFbo() + : m_fboRenderer(0) + , m_renderer(0) +{ +} + +PyFbo::~PyFbo() +{ + if (m_renderer) delete m_renderer; + // Do not delete m_fboRenderer, because we don't own it! +} + +void PyFbo::setRenderer(QVariant rendererRef) +{ + if (rendererRef == m_rendererRef) + return; + m_rendererRef = rendererRef; + + // Delete the old python GL renderer. + if (m_renderer) { + delete m_renderer; + m_renderer = 0; + } + + // Create a renderer object from the reference. + m_renderer = new PyGLRenderer(m_rendererRef, false); + + // If we already have a PyFboRenderer, set its python GL renderer. + // Otherwise it will be set when createRenderer() is called. + if (m_fboRenderer) { + ((PyFboRenderer *)m_fboRenderer)->setRenderer(m_renderer); + } +} + +QQuickFramebufferObject::Renderer *PyFbo::createRenderer() const +{ + m_fboRenderer = new PyFboRenderer(); + + // If we already have a python GL renderer, set it now. + if (m_renderer) { + ((PyFboRenderer *)m_fboRenderer)->setRenderer(m_renderer); + } + return m_fboRenderer; +} diff --git a/src/pyfbo.h b/src/pyfbo.h new file mode 100644 index 0000000..aaa2518 --- /dev/null +++ b/src/pyfbo.h @@ -0,0 +1,49 @@ + +/** + * PyOtherSide: Asynchronous Python 3 Bindings for Qt 5 + * Copyright (c) 2014, Dennis Tomas + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + **/ + +#ifndef PYFBO_H +#define PYFBO_H + +#include "pyglrenderer.h" + +#include +#include + +class PyFbo : public QQuickFramebufferObject +{ + Q_OBJECT + Q_PROPERTY(QVariant renderer READ renderer WRITE setRenderer) + +public: + PyFbo(); + ~PyFbo(); + Renderer *createRenderer() const; + + QVariant renderer() const { return m_rendererRef; }; + void setRenderer(QVariant rendererRef); +private: + QVariant m_rendererRef; + + // We have to keep a pointer to the renderer created by createRenderer() + // around, for being able to swap out its PyGLRenderer when + // PyFboInSGRenderer::setRenderer() is called. + mutable Renderer *m_fboRenderer; + PyGLRenderer *m_renderer; +}; + +#endif diff --git a/src/pyglrenderer.cpp b/src/pyglrenderer.cpp index 9035e3e..4305969 100644 --- a/src/pyglrenderer.cpp +++ b/src/pyglrenderer.cpp @@ -25,12 +25,13 @@ #include -PyGLRenderer::PyGLRenderer(QVariant pyRenderer) +PyGLRenderer::PyGLRenderer(QVariant pyRenderer, bool useRect) : m_pyRendererObject(0) , m_renderMethod(0) , m_initialized(false) { m_pyRenderer = pyRenderer; + m_useRect = useRect; } PyGLRenderer::~PyGLRenderer() @@ -105,24 +106,31 @@ void PyGLRenderer::render() return; } - // Call the paintGL callback with arguments x, y, width, height. - // These are the boundaries in which the callback should render, - // though it may choose to ignore them and simply paint anywhere over - // (or below) the QML scene. - // (x, y) is the bottom left corner. - // (x + width, y + height) is the top right corner. - PyObject *x = PyLong_FromLong(m_rect.x()); - PyObject *y = PyLong_FromLong(m_rect.y()); - PyObject *width = PyLong_FromLong(m_rect.width()); - PyObject *height = PyLong_FromLong(m_rect.height()); - PyObject *args = PyTuple_Pack(4, x, y, width, height); - PyObject *o = PyObject_Call(renderMethod, args, NULL); + PyObject *o = NULL; + if (m_useRect) { + // Call the paintGL callback with arguments x, y, width, height. + // These are the boundaries in which the callback should render, + // though it may choose to ignore them and simply paint anywhere over + // (or below) the QML scene. + // (x, y) is the bottom left corner. + // (x + width, y + height) is the top right corner. + PyObject *x = PyLong_FromLong(m_rect.x()); + PyObject *y = PyLong_FromLong(m_rect.y()); + PyObject *width = PyLong_FromLong(m_rect.width()); + PyObject *height = PyLong_FromLong(m_rect.height()); + PyObject *args = PyTuple_Pack(4, x, y, width, height); + o = PyObject_Call(renderMethod, args, NULL); + Py_DECREF(x); + Py_DECREF(y); + Py_DECREF(width); + Py_DECREF(height); + Py_DECREF(args); + } else { + PyObject *args = PyTuple_New(0); + o = PyObject_Call(renderMethod, args, NULL); + Py_DECREF(args); + } if (o) Py_DECREF(o); else PyErr_PrintEx(0); - Py_DECREF(x); - Py_DECREF(y); - Py_DECREF(width); - Py_DECREF(height); - Py_DECREF(args); priv->leave(); } diff --git a/src/pyglrenderer.h b/src/pyglrenderer.h index f3d756e..d3fee5e 100644 --- a/src/pyglrenderer.h +++ b/src/pyglrenderer.h @@ -31,7 +31,7 @@ class PyGLRenderer : public QObject { Q_OBJECT public: - PyGLRenderer(QVariant pyRenderer); + PyGLRenderer(QVariant pyRenderer, bool useRect=true); ~PyGLRenderer(); void init(); @@ -46,6 +46,7 @@ private: PyObject *m_pyRendererObject; PyObject *m_renderMethod; bool m_initialized; + bool m_useRect; PyObject *getPyRendererObject(); PyObject *getRenderMethod(); diff --git a/src/pyotherside_plugin.cpp b/src/pyotherside_plugin.cpp index ef34276..56ec766 100644 --- a/src/pyotherside_plugin.cpp +++ b/src/pyotherside_plugin.cpp @@ -19,6 +19,7 @@ #include "qpython_priv.h" #include "qpython.h" #include "pyglarea.h" +#include "pyfbo.h" #include "qpython_imageprovider.h" #include "global_libpython_loader.h" #include "pythonlib_loader.h" @@ -68,4 +69,5 @@ PyOtherSideExtensionPlugin::registerTypes(const char *uri) qmlRegisterType(uri, 1, 3, PYOTHERSIDE_QPYTHON_NAME); qmlRegisterType(uri, 1, 4, PYOTHERSIDE_QPYTHON_NAME); qmlRegisterType(uri, 1, 5, PYOTHERSIDE_QPYGLAREA_NAME); + qmlRegisterType(uri, 1, 5, PYOTHERSIDE_PYFBO_NAME); } diff --git a/src/pyotherside_plugin.h b/src/pyotherside_plugin.h index 01ae0fa..59b7cde 100644 --- a/src/pyotherside_plugin.h +++ b/src/pyotherside_plugin.h @@ -26,6 +26,7 @@ #define PYOTHERSIDE_IMAGEPROVIDER_ID "python" #define PYOTHERSIDE_QPYTHON_NAME "Python" #define PYOTHERSIDE_QPYGLAREA_NAME "PyGLArea" +#define PYOTHERSIDE_PYFBO_NAME "PyFBO" class Q_DECL_EXPORT PyOtherSideExtensionPlugin : public QQmlExtensionPlugin { Q_OBJECT diff --git a/src/src.pro b/src/src.pro index 3320dde..cc3e973 100644 --- a/src/src.pro +++ b/src/src.pro @@ -32,6 +32,10 @@ HEADERS += qpython_imageprovider.h SOURCES += pyglarea.cpp pyglrenderer.cpp HEADERS += pyglarea.h pyglrenderer.h +# PyGLFbo +SOURCES += pyfbo.cpp +HEADERS += pyfbo.h + # Importer from Qt Resources RESOURCES += qrc_importer.qrc From b1cd1781d3717b63d1c17704f50e2a4c754c107f Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Sun, 19 Oct 2014 08:53:52 +0200 Subject: [PATCH 12/20] Use static_cast. --- src/pyfbo.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyfbo.cpp b/src/pyfbo.cpp index 4e78a26..7c6c843 100644 --- a/src/pyfbo.cpp +++ b/src/pyfbo.cpp @@ -82,7 +82,7 @@ void PyFbo::setRenderer(QVariant rendererRef) // If we already have a PyFboRenderer, set its python GL renderer. // Otherwise it will be set when createRenderer() is called. if (m_fboRenderer) { - ((PyFboRenderer *)m_fboRenderer)->setRenderer(m_renderer); + static_cast(m_fboRenderer)->setRenderer(m_renderer); } } @@ -92,7 +92,7 @@ QQuickFramebufferObject::Renderer *PyFbo::createRenderer() const // If we already have a python GL renderer, set it now. if (m_renderer) { - ((PyFboRenderer *)m_fboRenderer)->setRenderer(m_renderer); + static_cast(m_fboRenderer)->setRenderer(m_renderer); } return m_fboRenderer; } From 1399072a9ccdb85a40f69e455adf7790f75e1bd6 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Sun, 19 Oct 2014 12:41:57 +0200 Subject: [PATCH 13/20] Fixed API usage in PyGLArea example. --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 164e63e..723d847 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1011,7 +1011,7 @@ http://qt-project.org/doc/qt-5/qtquick-scenegraph-openglunderqml-example.html. onTChanged: { if (renderer) { - py.callMethod(renderer, 'set_t', [t], update); + py.call(py.getattr(renderer, 'set_t'), [t], update); } } } From 00abfc8570181acf4cc2df34fb27a9b11a614039 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Sun, 19 Oct 2014 19:19:36 +0200 Subject: [PATCH 14/20] No need for PyGLRenderer to subclass QObject. --- src/pyglrenderer.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pyglrenderer.h b/src/pyglrenderer.h index d3fee5e..ff93a10 100644 --- a/src/pyglrenderer.h +++ b/src/pyglrenderer.h @@ -24,11 +24,9 @@ #include #include #include -#include -class PyGLRenderer : public QObject { - Q_OBJECT +class PyGLRenderer { public: PyGLRenderer(QVariant pyRenderer, bool useRect=true); From 2662c423b4af7c425107ff943a6d576ee518cd26 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Sun, 19 Oct 2014 20:02:01 +0200 Subject: [PATCH 15/20] Clean up PyGLRenderers before replacing them. --- src/pyfbo.cpp | 49 ++++++++++++++++++++++++++---------------------- src/pyfbo.h | 6 ++---- src/pyglarea.cpp | 39 ++++++++++++++++++++++++++------------ src/pyglarea.h | 2 ++ 4 files changed, 58 insertions(+), 38 deletions(-) diff --git a/src/pyfbo.cpp b/src/pyfbo.cpp index 7c6c843..2ed4134 100644 --- a/src/pyfbo.cpp +++ b/src/pyfbo.cpp @@ -26,20 +26,40 @@ class PyFboRenderer : public QQuickFramebufferObject::Renderer public: PyFboRenderer() : m_renderer(0) + , m_oldRenderer(0) { } + ~PyFboRenderer() + { + if (m_renderer) { + delete m_renderer; + m_renderer = 0; + } + if (m_oldRenderer) { + delete m_oldRenderer; + m_oldRenderer = 0; + } + } + void render() { + if (m_oldRenderer) { + m_oldRenderer->cleanup(); + delete m_oldRenderer; + m_oldRenderer = 0; + } if (!m_renderer) { return; } m_renderer->init(); m_renderer->render(); - update(); } - void setRenderer(PyGLRenderer *renderer) { - m_renderer = renderer; + void setRenderer(QVariant rendererRef) { + // Defer deleting the old renderer until render() is called, + // when we have an OpenGL context. + m_oldRenderer = m_renderer; + m_renderer = new PyGLRenderer(rendererRef, false); update(); } @@ -50,39 +70,24 @@ public: } private: PyGLRenderer *m_renderer; + PyGLRenderer *m_oldRenderer; }; PyFbo::PyFbo() : m_fboRenderer(0) - , m_renderer(0) { } -PyFbo::~PyFbo() -{ - if (m_renderer) delete m_renderer; - // Do not delete m_fboRenderer, because we don't own it! -} - void PyFbo::setRenderer(QVariant rendererRef) { if (rendererRef == m_rendererRef) return; m_rendererRef = rendererRef; - // Delete the old python GL renderer. - if (m_renderer) { - delete m_renderer; - m_renderer = 0; - } - - // Create a renderer object from the reference. - m_renderer = new PyGLRenderer(m_rendererRef, false); - // If we already have a PyFboRenderer, set its python GL renderer. // Otherwise it will be set when createRenderer() is called. if (m_fboRenderer) { - static_cast(m_fboRenderer)->setRenderer(m_renderer); + static_cast(m_fboRenderer)->setRenderer(m_rendererRef); } } @@ -91,8 +96,8 @@ QQuickFramebufferObject::Renderer *PyFbo::createRenderer() const m_fboRenderer = new PyFboRenderer(); // If we already have a python GL renderer, set it now. - if (m_renderer) { - static_cast(m_fboRenderer)->setRenderer(m_renderer); + if (!m_rendererRef.isNull()) { + static_cast(m_fboRenderer)->setRenderer(m_rendererRef); } return m_fboRenderer; } diff --git a/src/pyfbo.h b/src/pyfbo.h index aaa2518..28f1c7a 100644 --- a/src/pyfbo.h +++ b/src/pyfbo.h @@ -31,7 +31,6 @@ class PyFbo : public QQuickFramebufferObject public: PyFbo(); - ~PyFbo(); Renderer *createRenderer() const; QVariant renderer() const { return m_rendererRef; }; @@ -40,10 +39,9 @@ private: QVariant m_rendererRef; // We have to keep a pointer to the renderer created by createRenderer() - // around, for being able to swap out its PyGLRenderer when - // PyFboInSGRenderer::setRenderer() is called. + // around, for being able to swap out its PyGLRenderer + // when setRenderer() is called. mutable Renderer *m_fboRenderer; - PyGLRenderer *m_renderer; }; #endif diff --git a/src/pyglarea.cpp b/src/pyglarea.cpp index 4bc408e..4bb3d46 100644 --- a/src/pyglarea.cpp +++ b/src/pyglarea.cpp @@ -29,6 +29,8 @@ PyGLArea::PyGLArea() : m_before(false) , m_renderer(0) + , m_rendererChanged(false) + , m_beforeChanged(true) { connect(this, SIGNAL(windowChanged(QQuickWindow*)), this, SLOT(handleWindowChanged(QQuickWindow*))); } @@ -46,10 +48,10 @@ void PyGLArea::setRenderer(QVariant renderer) if (renderer == m_pyRenderer) return; m_pyRenderer = renderer; - if (m_renderer) { - delete m_renderer; - m_renderer = 0; - } + + // Defer creating the PyGLRenderer until sync() is called, + // when we have an OpenGL context. + m_rendererChanged = true; update(); } @@ -58,10 +60,8 @@ void PyGLArea::setBefore(bool before) if (before == m_before) return; m_before = before; - if (m_renderer) { - delete m_renderer; - m_renderer = 0; - } + + m_beforeChanged = true; update(); } @@ -80,10 +80,9 @@ void PyGLArea::update() { void PyGLArea::sync() { - if (!m_renderer && !m_pyRenderer.isNull()) { + if (m_beforeChanged) { disconnect(window(), SIGNAL(beforeRendering()), this, SLOT(render())); disconnect(window(), SIGNAL(afterRendering()), this, SLOT(render())); - m_renderer = new PyGLRenderer(m_pyRenderer); if (m_before) { // If we allow QML to do the clearing, they would clear what we paint // and nothing would show. @@ -93,11 +92,28 @@ void PyGLArea::sync() window()->setClearBeforeRendering(true); connect(window(), SIGNAL(afterRendering()), this, SLOT(render()), Qt::DirectConnection); } + m_beforeChanged = false; + } + + if (m_rendererChanged) { + if (m_renderer) { + m_renderer->cleanup(); + delete m_renderer; + m_renderer = 0; + } + if (!m_pyRenderer.isNull()) { + m_renderer = new PyGLRenderer(m_pyRenderer); + m_renderer->init(); + window()->resetOpenGLState(); + } + m_rendererChanged = false; } } void PyGLArea::render() { + if (!m_renderer) + return; QPointF pos = mapToScene(QPointF(.0, .0)); m_renderer->setRect( QRect( @@ -105,12 +121,11 @@ void PyGLArea::render() (long)this->width(), (long)this->height() ) ); - m_renderer->init(); m_renderer->render(); window()->resetOpenGLState(); } void PyGLArea::cleanup() { - m_renderer->cleanup(); + if (m_renderer) m_renderer->cleanup(); } diff --git a/src/pyglarea.h b/src/pyglarea.h index cc7d3c2..864849d 100644 --- a/src/pyglarea.h +++ b/src/pyglarea.h @@ -58,6 +58,8 @@ private: QVariant m_pyRenderer; bool m_before; PyGLRenderer *m_renderer; + bool m_rendererChanged; + bool m_beforeChanged; }; #endif /* PYOTHERSIDE_PYGLAREA_H */ From 1ec78f422e6cf6080781e1804628e7a025acbba7 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Sun, 19 Oct 2014 20:08:21 +0200 Subject: [PATCH 16/20] Don't clean up an uninitialized renderer. --- src/pyglrenderer.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pyglrenderer.cpp b/src/pyglrenderer.cpp index 4305969..4b9ed57 100644 --- a/src/pyglrenderer.cpp +++ b/src/pyglrenderer.cpp @@ -136,8 +136,10 @@ void PyGLRenderer::render() void PyGLRenderer::cleanup() { - QPythonPriv *priv = QPythonPriv::instance(); - priv->enter(); + if (!m_initialized) + return; + + ENSURE_GIL_STATE; PyObject *pyRendererObject = getPyRendererObject(); if (!pyRendererObject || pyRendererObject == Py_None || @@ -157,7 +159,7 @@ void PyGLRenderer::cleanup() PyObject *args = PyTuple_New(0); PyObject *o = PyObject_Call(cleanupMethod, args, NULL); if (o) Py_DECREF(o); else PyErr_PrintEx(0); - m_initialized = true; + m_initialized = false; Py_DECREF(args); Py_DECREF(cleanupMethod); priv->leave(); From 092370631ae6c77be1f9883f596942b2e1f0b316 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Sun, 19 Oct 2014 20:40:11 +0200 Subject: [PATCH 17/20] Use proper way of synchronizing PyFbo and PyFboRenderer. --- src/pyfbo.cpp | 62 +++++++++++++++++++++------------------------------ src/pyfbo.h | 5 ----- 2 files changed, 26 insertions(+), 41 deletions(-) diff --git a/src/pyfbo.cpp b/src/pyfbo.cpp index 2ed4134..b34c493 100644 --- a/src/pyfbo.cpp +++ b/src/pyfbo.cpp @@ -26,7 +26,6 @@ class PyFboRenderer : public QQuickFramebufferObject::Renderer public: PyFboRenderer() : m_renderer(0) - , m_oldRenderer(0) { } @@ -36,45 +35,47 @@ public: delete m_renderer; m_renderer = 0; } - if (m_oldRenderer) { - delete m_oldRenderer; - m_oldRenderer = 0; - } } - void render() { - if (m_oldRenderer) { - m_oldRenderer->cleanup(); - delete m_oldRenderer; - m_oldRenderer = 0; - } - if (!m_renderer) { + void render() + { + if (!m_renderer) return; - } - m_renderer->init(); m_renderer->render(); } - void setRenderer(QVariant rendererRef) { - // Defer deleting the old renderer until render() is called, - // when we have an OpenGL context. - m_oldRenderer = m_renderer; - m_renderer = new PyGLRenderer(rendererRef, false); - update(); + void synchronize(QQuickFramebufferObject *item) + { + PyFbo *pyFbo = static_cast(item); + + if (pyFbo->renderer() == m_rendererRef) + return; + + if (m_renderer) { + m_renderer->cleanup(); + delete m_renderer; + m_renderer = 0; + } + + m_rendererRef = pyFbo->renderer(); + if (!m_rendererRef.isNull()) { + m_renderer = new PyGLRenderer(m_rendererRef, false); + m_renderer->init(); + } } - QOpenGLFramebufferObject *createFramebufferObject(const QSize &size) { + QOpenGLFramebufferObject *createFramebufferObject(const QSize &size) + { QOpenGLFramebufferObjectFormat format; // TODO: get the FBO format from the PyGLRenderer. return new QOpenGLFramebufferObject(size, format); } private: + QVariant m_rendererRef; PyGLRenderer *m_renderer; - PyGLRenderer *m_oldRenderer; }; PyFbo::PyFbo() - : m_fboRenderer(0) { } @@ -83,21 +84,10 @@ void PyFbo::setRenderer(QVariant rendererRef) if (rendererRef == m_rendererRef) return; m_rendererRef = rendererRef; - - // If we already have a PyFboRenderer, set its python GL renderer. - // Otherwise it will be set when createRenderer() is called. - if (m_fboRenderer) { - static_cast(m_fboRenderer)->setRenderer(m_rendererRef); - } + update(); } QQuickFramebufferObject::Renderer *PyFbo::createRenderer() const { - m_fboRenderer = new PyFboRenderer(); - - // If we already have a python GL renderer, set it now. - if (!m_rendererRef.isNull()) { - static_cast(m_fboRenderer)->setRenderer(m_rendererRef); - } - return m_fboRenderer; + return new PyFboRenderer(); } diff --git a/src/pyfbo.h b/src/pyfbo.h index 28f1c7a..1b59cb8 100644 --- a/src/pyfbo.h +++ b/src/pyfbo.h @@ -37,11 +37,6 @@ public: void setRenderer(QVariant rendererRef); private: QVariant m_rendererRef; - - // We have to keep a pointer to the renderer created by createRenderer() - // around, for being able to swap out its PyGLRenderer - // when setRenderer() is called. - mutable Renderer *m_fboRenderer; }; #endif From 77ecb76492f26d62cdd4f3f418307c9a9ac7a000 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Mon, 20 Oct 2014 08:16:54 +0200 Subject: [PATCH 18/20] Unified renderer interface for PyFBO and PyGLArea. --- docs/index.rst | 26 +-- examples/pyfbo.py | 74 -------- examples/pyfbo.qml | 4 +- examples/pyglarea.qml | 4 +- examples/{pyglarea.py => renderer.py} | 6 +- src/pyfbo.cpp | 44 +++-- src/pyfbo.h | 1 - src/pyglarea.cpp | 4 +- src/pyglrenderer.cpp | 251 ++++++++++---------------- src/pyglrenderer.h | 14 +- src/src.pro | 2 +- 11 files changed, 156 insertions(+), 274 deletions(-) delete mode 100644 examples/pyfbo.py rename examples/{pyglarea.py => renderer.py} (95%) diff --git a/docs/index.rst b/docs/index.rst index 723d847..f0b3110 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -544,16 +544,20 @@ following properties: Initialize OpenGL resources required for rendering. This method is optional. -.. function:: IRenderer.render(x, y, width, height) +.. function:: IRenderer.reshape(x, y, width, height) - Render within the given area. - It is the renderer's responsibility to confine itself to that area. - Also, it must unbind any used resources to leave the context in a clean - state. + Called when the geometry has changed. ``(x, y)`` is the position of the bottom left corner of the area, in window coordinates, e.g. (0, 0) is the bottom left corner of the window. +.. function:: IRenderer.render() + + Render to the OpenGL context. + + It is the renderer's responsibility to unbind any used resources to leave + the context in a clean state. + .. function:: IRenderer.cleanup() Free any resources allocated by :func:`IRenderer.init`. @@ -905,7 +909,7 @@ The example below shows how to do raw OpenGL rendering in PyOpenGL using a PyGLArea. It has been adapted from the tutorial in the Qt documentation at http://qt-project.org/doc/qt-5/qtquick-scenegraph-openglunderqml-example.html. -**pyglarea.py** +**renderer.py** .. code-block:: python @@ -957,10 +961,10 @@ http://qt-project.org/doc/qt-5/qtquick-scenegraph-openglunderqml-example.html. self.vertices_attr = glGetAttribLocation(self.program, b'vertices') self.t_attr = glGetUniformLocation(self.program, b't') - def render(self, x, y, width, height): - glMatrixMode(GL_PROJECTION) - glLoadIdentity() + def reshape(self, x, y, width, height): glViewport(x, y, width, height) + + def render(self): glUseProgram(self.program) try: glDisable(GL_DEPTH_TEST) @@ -1041,8 +1045,8 @@ http://qt-project.org/doc/qt-5/qtquick-scenegraph-openglunderqml-example.html. Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); - importModule('pyglarea', function () { - call('pyglarea.Renderer', [], function (renderer) { + importModule('renderer', function () { + call('renderer', [], function (renderer) { glArea.renderer = renderer; }); }); diff --git a/examples/pyfbo.py b/examples/pyfbo.py deleted file mode 100644 index 029aed5..0000000 --- a/examples/pyfbo.py +++ /dev/null @@ -1,74 +0,0 @@ -import numpy - -from OpenGL.GL import * -from OpenGL.GL.shaders import compileShader, compileProgram - - -VERTEX_SHADER = """#version 130 -attribute highp vec4 vertices; -varying highp vec2 coords; - -void main() { - gl_Position = vertices; - coords = vertices.xy; -} -""" - -FRAGMENT_SHADER = """#version 130 -uniform lowp float t; -varying highp vec2 coords; -void main() { - lowp float i = 1. - (pow(abs(coords.x), 4.) + pow(abs(coords.y), 4.)); - i = smoothstep(t - 0.8, t + 0.8, i); - i = floor(i * 20.) / 20.; - gl_FragColor = vec4(coords * .5 + .5, i, i); -} -""" - - -class Renderer(object): - - def __init__(self): - self.t = 0.0 - self.values = numpy.array([ - -1.0, -1.0, - 1.0, -1.0, - -1.0, 1.0, - 1.0, 1.0 - ], dtype=numpy.float32) - - def set_t(self, t): - self.t = t - - def init(self): - self.vertexbuffer = glGenBuffers(1) - vertex_shader = compileShader(VERTEX_SHADER, GL_VERTEX_SHADER) - fragment_shader = compileShader(FRAGMENT_SHADER, GL_FRAGMENT_SHADER) - self.program = compileProgram(vertex_shader, fragment_shader) - self.vertices_attr = glGetAttribLocation(self.program, b'vertices') - self.t_attr = glGetUniformLocation(self.program, b't') - - def render(self): - glUseProgram(self.program) - try: - glDisable(GL_DEPTH_TEST) - glClearColor(0, 0, 0, 1) - glClear(GL_COLOR_BUFFER_BIT) - glEnable(GL_BLEND) - glBlendFunc(GL_SRC_ALPHA, GL_ONE) - - glBindBuffer(GL_ARRAY_BUFFER, self.vertexbuffer) - glEnableVertexAttribArray(self.vertices_attr) - glBufferData(GL_ARRAY_BUFFER, self.values, GL_STATIC_DRAW) - glVertexAttribPointer(self.vertices_attr, 2, GL_FLOAT, GL_FALSE, 0, None) - glUniform1f(self.t_attr, self.t) - - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4) - finally: - glDisableVertexAttribArray(self.vertices_attr) - glBindBuffer(GL_ARRAY_BUFFER, 0) - glUseProgram(0) - - def cleanup(self): - glDeleteProgram(self.program) - glDeleteBuffers(1, [self.vertexbuffer]) diff --git a/examples/pyfbo.qml b/examples/pyfbo.qml index 5500589..8acc143 100644 --- a/examples/pyfbo.qml +++ b/examples/pyfbo.qml @@ -147,8 +147,8 @@ Item { Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); - importModule('pyfbo', function () { - call('pyfbo.Renderer', [], function (renderer) { + importModule('renderer', function () { + call('renderer.Renderer', [], function (renderer) { fbo.renderer = renderer; }); }); diff --git a/examples/pyglarea.qml b/examples/pyglarea.qml index 1954dd4..3bd4619 100644 --- a/examples/pyglarea.qml +++ b/examples/pyglarea.qml @@ -50,8 +50,8 @@ Item { Component.onCompleted: { addImportPath(Qt.resolvedUrl('.')); - importModule('pyglarea', function () { - call('pyglarea.Renderer', [], function (renderer) { + importModule('renderer', function () { + call('renderer.Renderer', [], function (renderer) { glArea.renderer = renderer; }); }); diff --git a/examples/pyglarea.py b/examples/renderer.py similarity index 95% rename from examples/pyglarea.py rename to examples/renderer.py index cbb1379..f2e07a7 100644 --- a/examples/pyglarea.py +++ b/examples/renderer.py @@ -46,10 +46,10 @@ class Renderer(object): self.vertices_attr = glGetAttribLocation(self.program, b'vertices') self.t_attr = glGetUniformLocation(self.program, b't') - def render(self, x, y, width, height): - glMatrixMode(GL_PROJECTION) - glLoadIdentity() + def reshape(self, x, y, width, height): glViewport(x, y, width, height) + + def render(self): glUseProgram(self.program) try: glDisable(GL_DEPTH_TEST) diff --git a/src/pyfbo.cpp b/src/pyfbo.cpp index b34c493..2647211 100644 --- a/src/pyfbo.cpp +++ b/src/pyfbo.cpp @@ -19,6 +19,7 @@ #include "pyfbo.h" #include +#include class PyFboRenderer : public QQuickFramebufferObject::Renderer @@ -26,6 +27,7 @@ class PyFboRenderer : public QQuickFramebufferObject::Renderer public: PyFboRenderer() : m_renderer(0) + , m_size(0, 0) { } @@ -39,33 +41,41 @@ public: void render() { - if (!m_renderer) - return; - m_renderer->render(); + if (m_renderer) + m_renderer->render(); } void synchronize(QQuickFramebufferObject *item) { PyFbo *pyFbo = static_cast(item); - if (pyFbo->renderer() == m_rendererRef) - return; - - if (m_renderer) { - m_renderer->cleanup(); - delete m_renderer; - m_renderer = 0; + if (pyFbo->renderer() != m_rendererRef) { + // The renderer has changed. + if (m_renderer) { + m_renderer->cleanup(); + delete m_renderer; + m_renderer = 0; + } + m_rendererRef = pyFbo->renderer(); + if (!m_rendererRef.isNull()) { + m_renderer = new PyGLRenderer(m_rendererRef); + m_renderer->init(); + m_sizeChanged = true; + } } - m_rendererRef = pyFbo->renderer(); - if (!m_rendererRef.isNull()) { - m_renderer = new PyGLRenderer(m_rendererRef, false); - m_renderer->init(); + if (m_renderer && m_sizeChanged) { + // The size has changed. + m_renderer->reshape(QRect(QPoint(0, 0), m_size)); + m_sizeChanged = false; + update(); } } QOpenGLFramebufferObject *createFramebufferObject(const QSize &size) { + m_size = size; + m_sizeChanged = true; QOpenGLFramebufferObjectFormat format; // TODO: get the FBO format from the PyGLRenderer. return new QOpenGLFramebufferObject(size, format); @@ -73,12 +83,10 @@ public: private: QVariant m_rendererRef; PyGLRenderer *m_renderer; + QSize m_size; + bool m_sizeChanged; }; -PyFbo::PyFbo() -{ -} - void PyFbo::setRenderer(QVariant rendererRef) { if (rendererRef == m_rendererRef) diff --git a/src/pyfbo.h b/src/pyfbo.h index 1b59cb8..da3274a 100644 --- a/src/pyfbo.h +++ b/src/pyfbo.h @@ -30,7 +30,6 @@ class PyFbo : public QQuickFramebufferObject Q_PROPERTY(QVariant renderer READ renderer WRITE setRenderer) public: - PyFbo(); Renderer *createRenderer() const; QVariant renderer() const { return m_rendererRef; }; diff --git a/src/pyglarea.cpp b/src/pyglarea.cpp index 4bb3d46..4981797 100644 --- a/src/pyglarea.cpp +++ b/src/pyglarea.cpp @@ -115,10 +115,10 @@ void PyGLArea::render() if (!m_renderer) return; QPointF pos = mapToScene(QPointF(.0, .0)); - m_renderer->setRect( + m_renderer->reshape( QRect( (long)pos.x(), (long)(window()->height() - this->height() - pos.y()), - (long)this->width(), (long)this->height() + this->width(), this->height() ) ); m_renderer->render(); diff --git a/src/pyglrenderer.cpp b/src/pyglrenderer.cpp index 4b9ed57..39d87ea 100644 --- a/src/pyglrenderer.cpp +++ b/src/pyglrenderer.cpp @@ -25,180 +25,129 @@ #include -PyGLRenderer::PyGLRenderer(QVariant pyRenderer, bool useRect) +PyGLRenderer::PyGLRenderer(QVariant pyRenderer) : m_pyRendererObject(0) + , m_initMethod(0) + , m_reshapeMethod(0) , m_renderMethod(0) + , m_cleanupMethod(0) , m_initialized(false) { - m_pyRenderer = pyRenderer; - m_useRect = useRect; + + ENSURE_GIL_STATE; + + if (pyRenderer.userType() != qMetaTypeId()) { + qWarning() << "Renderer must be of type PyObjectRef (got " + << pyRenderer << ")."; + return; + } + + m_pyRendererObject = pyRenderer.value().newRef(); + + if (PyObject_HasAttrString(m_pyRendererObject, "render")) { + m_renderMethod = PyObject_GetAttrString(m_pyRendererObject, "render"); + if (!m_renderMethod) { + qWarning() << "Error getting render method of renderer."; + PyErr_PrintEx(0); + } + } else { + qWarning() << "Renderer has no render method."; + } + + if (PyObject_HasAttrString(m_pyRendererObject, "init")) { + m_initMethod = PyObject_GetAttrString(m_pyRendererObject, "init"); + if (!m_initMethod) { + qWarning() << "Error getting init method of renderer."; + PyErr_PrintEx(0); + } + } + + if (PyObject_HasAttrString(m_pyRendererObject, "reshape")) { + m_reshapeMethod = PyObject_GetAttrString(m_pyRendererObject, "reshape"); + if (!m_reshapeMethod) { + qWarning() << "Error getting reshape method of renderer."; + PyErr_PrintEx(0); + } + } + + if (PyObject_HasAttrString(m_pyRendererObject, "cleanup")) { + m_cleanupMethod = PyObject_GetAttrString(m_pyRendererObject, "cleanup"); + if (!m_cleanupMethod) { + qWarning() << "Error getting cleanup method of renderer."; + PyErr_PrintEx(0); + } + } } PyGLRenderer::~PyGLRenderer() { - if (m_pyRendererObject) { - QPythonPriv *priv = QPythonPriv::instance(); - priv->enter(); - Py_CLEAR(m_pyRendererObject); - if (m_renderMethod) - Py_DECREF(m_renderMethod); - priv->leave(); - m_pyRendererObject = 0; - m_renderMethod = 0; - } -} - -void PyGLRenderer::setRect(QRect rect) { - m_rect = rect; + ENSURE_GIL_STATE; + Py_CLEAR(m_initMethod); + Py_CLEAR(m_reshapeMethod); + Py_CLEAR(m_renderMethod); + Py_CLEAR(m_cleanupMethod); + Py_CLEAR(m_pyRendererObject); } void PyGLRenderer::init() { - if (m_initialized) - return; - - QPythonPriv *priv = QPythonPriv::instance(); - priv->enter(); - - PyObject *pyRendererObject = getPyRendererObject(); - if (!pyRendererObject || pyRendererObject == Py_None) { - priv->leave(); - return; - } - - if (!PyObject_HasAttrString(m_pyRendererObject, "init")) { - // Optional init() method not found, consider the renderer initialized. - priv->leave(); - m_initialized = true; - return; - } - - PyObject *initMethod = PyObject_GetAttrString(m_pyRendererObject, "init"); - if (!initMethod) { - qWarning() << "Failed to get init method of renderer."; - PyErr_PrintEx(0); - priv->leave(); - return; - } - - PyObject *args = PyTuple_New(0); - PyObject *o = PyObject_Call(initMethod, args, NULL); - if (o) Py_DECREF(o); else PyErr_PrintEx(0); - Py_DECREF(args); - Py_DECREF(initMethod); - priv->leave(); - m_initialized = true; -} - - -void PyGLRenderer::render() -{ - if (!m_initialized) - return; - - QPythonPriv *priv = QPythonPriv::instance(); - priv->enter(); - - PyObject *renderMethod = getRenderMethod(); - if (!renderMethod) { - qWarning() << "Failed to get render method of renderer."; - PyErr_PrintEx(0); - priv->leave(); - return; - } - - PyObject *o = NULL; - if (m_useRect) { - // Call the paintGL callback with arguments x, y, width, height. - // These are the boundaries in which the callback should render, - // though it may choose to ignore them and simply paint anywhere over - // (or below) the QML scene. - // (x, y) is the bottom left corner. - // (x + width, y + height) is the top right corner. - PyObject *x = PyLong_FromLong(m_rect.x()); - PyObject *y = PyLong_FromLong(m_rect.y()); - PyObject *width = PyLong_FromLong(m_rect.width()); - PyObject *height = PyLong_FromLong(m_rect.height()); - PyObject *args = PyTuple_Pack(4, x, y, width, height); - o = PyObject_Call(renderMethod, args, NULL); - Py_DECREF(x); - Py_DECREF(y); - Py_DECREF(width); - Py_DECREF(height); - Py_DECREF(args); - } else { - PyObject *args = PyTuple_New(0); - o = PyObject_Call(renderMethod, args, NULL); - Py_DECREF(args); - } - if (o) Py_DECREF(o); else PyErr_PrintEx(0); - priv->leave(); -} - -void PyGLRenderer::cleanup() -{ - if (!m_initialized) + if (m_initialized || !m_initMethod) return; ENSURE_GIL_STATE; - PyObject *pyRendererObject = getPyRendererObject(); - if (!pyRendererObject || pyRendererObject == Py_None || - !PyObject_HasAttrString(m_pyRendererObject, "cleanup")) { - priv->leave(); - return; - } - - PyObject *cleanupMethod = PyObject_GetAttrString(m_pyRendererObject, "cleanup"); - if (!cleanupMethod) { - qWarning() << "Failed to get cleanup method of renderer."; - PyErr_PrintEx(0); - priv->leave(); - return; - } - PyObject *args = PyTuple_New(0); - PyObject *o = PyObject_Call(cleanupMethod, args, NULL); + PyObject *o = PyObject_Call(m_initMethod, args, NULL); if (o) Py_DECREF(o); else PyErr_PrintEx(0); - m_initialized = false; Py_DECREF(args); - Py_DECREF(cleanupMethod); + m_initialized = true; +} + +void PyGLRenderer::reshape(QRect geometry) +{ + if (!m_initialized || !m_reshapeMethod) + return; + + QPythonPriv *priv = QPythonPriv::instance(); + priv->enter(); + + // Call the reshape callback with arguments x, y, width, height. + // These are the boundaries in which the callback should render, + // though it may choose to ignore them and simply paint anywhere over + // (or below) the QML scene. + // (x, y) is the bottom left corner. + // (x + width, y + height) is the top right corner. + PyObject *args = Py_BuildValue( + "llll", geometry.x(), geometry.y(), geometry.width(), geometry.height() + ); + PyObject *o = PyObject_Call(m_reshapeMethod, args, NULL); + Py_DECREF(args); + if (o) Py_DECREF(o); else PyErr_PrintEx(0); priv->leave(); } -PyObject *PyGLRenderer::getPyRendererObject() { - if (m_pyRendererObject) { - return m_pyRendererObject; - } - if (m_pyRenderer.userType() != qMetaTypeId()) { - qWarning() << "Renderer must be of type PyObjectRef (got " - << m_pyRenderer << ")."; - return NULL; - } - m_pyRendererObject = m_pyRenderer.value().newRef(); - return m_pyRendererObject; +void PyGLRenderer::render() +{ + if (!m_initialized || !m_renderMethod) + return; + + ENSURE_GIL_STATE; + + PyObject *args = PyTuple_New(0); + PyObject *o = PyObject_Call(m_renderMethod, args, NULL); + Py_DECREF(args); + if (o) Py_DECREF(o); else PyErr_PrintEx(0); } -PyObject *PyGLRenderer::getRenderMethod() { - if (m_renderMethod) { - return m_renderMethod; - } +void PyGLRenderer::cleanup() +{ + if (!m_initialized || !m_cleanupMethod) + return; - PyObject *pyRendererObject = getPyRendererObject(); - if (!pyRendererObject || pyRendererObject == Py_None) { - return NULL; - } + ENSURE_GIL_STATE; - if (!PyObject_HasAttrString(m_pyRendererObject, "render")) { - qWarning() << "Renderer has no render method."; - return NULL; - } - - PyObject *m_renderMethod = PyObject_GetAttrString(m_pyRendererObject, "render"); - if (!m_renderMethod) { - qWarning() << "Failed to get render method of renderer."; - PyErr_PrintEx(0); - return NULL; - } - - return m_renderMethod; + PyObject *args = PyTuple_New(0); + PyObject *o = PyObject_Call(m_cleanupMethod, args, NULL); + if (o) Py_DECREF(o); else PyErr_PrintEx(0); + m_initialized = false; + Py_DECREF(args); } diff --git a/src/pyglrenderer.h b/src/pyglrenderer.h index ff93a10..92e2653 100644 --- a/src/pyglrenderer.h +++ b/src/pyglrenderer.h @@ -29,25 +29,21 @@ class PyGLRenderer { public: - PyGLRenderer(QVariant pyRenderer, bool useRect=true); + PyGLRenderer(QVariant pyRenderer); ~PyGLRenderer(); void init(); + void reshape(QRect geometry); void render(); void cleanup(); - void setRect(QRect rect); - private: - QRect m_rect; - QVariant m_pyRenderer; PyObject *m_pyRendererObject; + PyObject *m_initMethod; + PyObject *m_reshapeMethod; PyObject *m_renderMethod; + PyObject *m_cleanupMethod; bool m_initialized; - bool m_useRect; - - PyObject *getPyRendererObject(); - PyObject *getRenderMethod(); }; #endif /* PYOTHERSIDE_PYGLRENDERER_H */ diff --git a/src/src.pro b/src/src.pro index cc3e973..b7340be 100644 --- a/src/src.pro +++ b/src/src.pro @@ -32,7 +32,7 @@ HEADERS += qpython_imageprovider.h SOURCES += pyglarea.cpp pyglrenderer.cpp HEADERS += pyglarea.h pyglrenderer.h -# PyGLFbo +# PyFBO SOURCES += pyfbo.cpp HEADERS += pyfbo.h From 72925419c20dc9dc251787ce58e98b5f004e9836 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Mon, 20 Oct 2014 17:02:10 +0200 Subject: [PATCH 19/20] Implemented PyObjectRef::operator==. --- src/pyobject_ref.cpp | 6 ++++++ src/pyobject_ref.h | 1 + 2 files changed, 7 insertions(+) diff --git a/src/pyobject_ref.cpp b/src/pyobject_ref.cpp index d1ccd10..245d4b7 100644 --- a/src/pyobject_ref.cpp +++ b/src/pyobject_ref.cpp @@ -67,6 +67,12 @@ PyObjectRef::operator=(const PyObjectRef &other) return *this; } +bool +PyObjectRef::operator==(const PyObjectRef &other) +{ + return pyobject == other.pyobject; +} + PyObject * PyObjectRef::newRef() const { diff --git a/src/pyobject_ref.h b/src/pyobject_ref.h index 47b2ef3..efd5d14 100644 --- a/src/pyobject_ref.h +++ b/src/pyobject_ref.h @@ -30,6 +30,7 @@ class PyObjectRef { PyObjectRef(const PyObjectRef &other); virtual ~PyObjectRef(); PyObjectRef &operator=(const PyObjectRef &other); + bool operator==(const PyObjectRef &other); PyObject *newRef() const; From 37ba99e0bf79cf7d13b1339aba65e7a01c04e110 Mon Sep 17 00:00:00 2001 From: Dennis Tomas Date: Sat, 21 Feb 2015 12:36:23 +0100 Subject: [PATCH 20/20] Fixed GIL API usage in renderer. --- src/pyglrenderer.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pyglrenderer.cpp b/src/pyglrenderer.cpp index 39d87ea..a2c8240 100644 --- a/src/pyglrenderer.cpp +++ b/src/pyglrenderer.cpp @@ -20,6 +20,7 @@ #include "converter.h" #include "pyobject_ref.h" #include "pyglrenderer.h" +#include "ensure_gil_state.h" #include #include @@ -107,8 +108,7 @@ void PyGLRenderer::reshape(QRect geometry) if (!m_initialized || !m_reshapeMethod) return; - QPythonPriv *priv = QPythonPriv::instance(); - priv->enter(); + ENSURE_GIL_STATE; // Call the reshape callback with arguments x, y, width, height. // These are the boundaries in which the callback should render, @@ -122,7 +122,6 @@ void PyGLRenderer::reshape(QRect geometry) PyObject *o = PyObject_Call(m_reshapeMethod, args, NULL); Py_DECREF(args); if (o) Py_DECREF(o); else PyErr_PrintEx(0); - priv->leave(); } void PyGLRenderer::render()