Qt / openGL 纹理无法正常工作

Qt / openGL textures not working correctly

我在使用 openGL 纹理时遇到 python3 / qt5 / openGL 的奇怪行为。

在两个单独的 windows 中打开两个 QOpenGLWidgets 会导致两个 Widget 之间的 openGL 纹理混合 - 在第一个 widget 中更改纹理会导致在第二个 widget 中更改纹理。尽管它们应该有不同的 openGL 上下文..!

提供了演示此问题的代码(见下文)- 请先尝试,然后再提出解决方案。代码简洁地演示了两种情况:

1) 创建两个共享相同 parent Widget

的 QOpenGLWidget

2) 创建两个没有 parents

的 QOpenGLWidgets

为了修复案例 (2),我还尝试强制两个小部件共享相同的 OpenGLContext .. 但没有成功。

http://doc.qt.io/qt-5/qopenglwidget.html

表示如下:

Creating extra QOpenGLContext instances that share resources like textures with the QOpenGLWidget's context is also possible. 'Simply' pass the pointer returned from context() to QOpenGLContext::setShareContext() before calling QOpenGLContext::create()

是的,我可以实例化自定义 QOpenGLContext 以进行共享,但是无法强制 QOpenGLWidget 使用该自定义上下文:QOpenGLWidget 在某处(哪里?)自动实例化 QOpenGLContext ..我只能在 initializeGL 中访问它( "self.context()"),但此时上下文的“.create()”方法已被调用..! .. 因此无法在任何地方填充我的自定义 QOpenGLContext。

我还尝试了以下方法:我们创建了一个 class 变量

glcontext=None

在小部件的(请首先查看下面的完整示例)initializeGL 方法中,我正在尝试这样做:

if (VideoImageWidget.glcontext==None):
    print("VideoImageWidget: initializeGL: creating context for sharing")
    VideoImageWidget.glcontext=QtGui.QOpenGLContext()
    ok=VideoImageWidget.glcontext.create()
    print("VideoImageWidget: initializeGL: created context for sharing",VideoImageWidget.glcontext,ok)

context=self.context()
print("VideoImageWidget: initializeGL: automatically created context:",context)
context.setShareContext(VideoImageWidget.glcontext)
ok=context.create() # must call this ..
print("VideoImageWidget: initializeGL: recreated my context:",ok)

但这不起作用.. VideoImageWidget 不再显示任何图像。

这是一团糟!非常感谢帮助!

相关:

Is it possible to use the same OpenGL context between top-level windows in Qt?

How to share OpenGL context or data?

演示程序:

import sys
import time
from PyQt5 import QtWidgets, QtCore, QtGui # Qt5
from OpenGL.GL import *
from PIL import Image

"""
Demonstrating a bug (?) in QOpenGLWidget / Qt OpenGL insfrastructure :

You need:
  * to have two tiff images ("base.tif" and "2.tif") in the same directory
  * to remove/install some libraries:
  sudo apt-get install python3-pyqt5 pip3
  sudo apt-get remove python3-opengl                     # we want the most recent version of the opengl bindings
  sudo pip3 install PyOpenGL PyOpenGL_accelerate
  sudo pip3 install imutils

Usage:
  * look for the tag "TOGGLE HERE" below to switch between nested QWidgets / individual windows
  * run program with
  python3 context_test.py

What's going on here?
  * Press the button a few times : a new image appears to the first widget
  * The image should appear only to the first widget
  * .. but it appears in both widgets if we set "nested=False", i.e. when the widgets constitute individual windows
  * .. confirm this by clicking / resizing the second window after clicking the button a few times

Why this happens?
  * Qt creates some sort of "top-level" (?) opengl context that is referring to the same texture ids = bug ?


This code is licensed under the do-with-it-whatever-you-want license, written by Sampsa Riikonen, 2017
"""

def getImg(fname):
    im =QtGui.QImage(fname)
    im =im.convertToFormat(QtGui.QImage.Format_RGB888)
    ix =im.width()
    iy =im.height()
    ptr=im.bits()
    ptr.setsize(im.byteCount())
    return ptr.asstring(), ix, iy


class VideoImageWidget(QtWidgets.QOpenGLWidget): # http://doc.qt.io/qt-5/qopenglwidget.html # Qt5
    def __init__(self,parent=None):
        super().__init__(parent=parent)
        self.parent=parent

        self.baseimage, self.ix, self.iy =getImg("base.tif")

        self.gl_format=GL_RGB
        self.ratio     =1
        self.picratio  =1

    def changeTexture(self,image,ix,iy):
        glBindTexture(GL_TEXTURE_2D, self.tex) # this is the texture we will manipulate
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
        glTexImage2D(GL_TEXTURE_2D, 0, self.gl_format, ix, iy, 0, self.gl_format, GL_UNSIGNED_BYTE, image) # load bitmap to texture
        self.picratio=self.iy/self.ix

    def resetTexture(self):
        self.changeTexture(self.baseimage,self.ix,self.iy)

    def initializeGL(self):
        # "This function should set up any required OpenGL resources and state"
        glEnable(GL_TEXTURE_2D)
        self.tex = glGenTextures(1) # create a new texture
        # https://www.khronos.org/opengles/sdk/docs/man/xhtml/glGenTextures.xml
        # "it is guaranteed that none of the returned names was in use immediately before the call"
        print("VideoImageWidget: glGenTextures returned:",self.tex)
        self.resetTexture()

    def paintGL(self):
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)  # Clear The Screen And The Depth Buffer
        glLoadIdentity() # Reset The View
        glBindTexture(GL_TEXTURE_2D, self.tex) # this is the texture we will manipulate
        glBegin(GL_QUADS)
        dz=0
        r=self.ratio/self.picratio # screen h/w  // picture h/w
        if (r<1):   # screen wider than image
            dy=1
            dx=r
        elif (r>1): # screen taller than image
            dx=1
            dy=1/r
        else:
            dx=1
            dy=1
        glTexCoord2f(0.0, 0.0); glVertex3f(-dx, dy, dz)
        glTexCoord2f(1.0, 0.0); glVertex3f( dx, dy, dz)
        glTexCoord2f(1.0, 1.0); glVertex3f( dx,-dy, dz)
        glTexCoord2f(0.0, 1.0); glVertex3f(-dx,-dy, dz)
        glEnd()

    def resizeGL(self, width, height):
        """Called upon window resizing: reinitialize the viewport.
        """
        glViewport(0, 0, width, height)
        glLoadIdentity()
        glOrtho(-1, 1, 1, -1, -1, 1)
        self.ratio=height/width

    @QtCore.pyqtSlot(object)
    def frameReceived(self,frame):
        buf   =frame[0]
        width =frame[1]
        height=frame[2]
        print("VideoImageWidget updating with frame",width,height)
        self.changeTexture(buf,width,height)
        self.update()


class MyGui(QtWidgets.QMainWindow):

    f1 = QtCore.pyqtSignal(object)
    f2 = QtCore.pyqtSignal(object)

    def __init__(self,parent=None):
        super().__init__(parent)
        self.cw=QtWidgets.QWidget(self)
        self.setCentralWidget(self.cw)

        self.lay = QtWidgets.QVBoxLayout(self.cw)

        self.b   = QtWidgets.QPushButton("Send frame",self.cw)

        # *** TOGGLE HERE ***
        # nested=True   # *** widgets sitting in the QMainWindow
        nested=False    # *** individual windows

        self.lay.addWidget(self.b)
        if (nested):
            self.v1  = VideoImageWidget(parent=self.cw)
            self.v2  = VideoImageWidget(parent=self.cw)
            self.lay.addWidget(self.v1)
            self.lay.addWidget(self.v2)
        else:
            self.v1  = VideoImageWidget(parent=None)
            self.v2  = VideoImageWidget(parent=None)
            self.v1.show()
            self.v2.show()

        self.b.clicked. connect(self.clicked)
        self.f1.        connect(self.v1.frameReceived)

        self.newimage, self.ix, self.iy =getImg("2.tif")

    @QtCore.pyqtSlot()
    def clicked(self):
        print("emitting frame")
        self.f1.emit([self.newimage, self.ix, self.iy])  # update _only_ the first VideoImageWidget

if (__name__=="__main__"):
    app=QtWidgets.QApplication([])

    # *** Set this to apply context sharing ***
    # app.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
    """
    .. but that does not work for textures

    See the last comment on this post:
    
    "I've realised that this does not solve the problem in the question, as it does not actually share the context, but enables sharing of a subset of resources .."

    That is said also in the qt docs, but in an extremely implicit way..
    http://doc.qt.io/qt-5/qopenglwidget.html
    "Creating extra QOpenGLContext instances that share resources like textures .. "
    """

    print("OpenGL context sharing status:",app.testAttribute(QtCore.Qt.AA_ShareOpenGLContexts))
    mg=MyGui()
    mg.show()
    app.exec_()

我找到了修复示例的方法,但我不知道它是否是 "correct" 解决方案,因为我对 OpenGL 几乎一无所知(也许这有帮助)。

不管怎样,我所做的就是这样:

@QtCore.pyqtSlot(object)
def frameReceived(self, frame):
    self.makeCurrent()
    ...

根据 makeCurrent 的文档:

It is not necessary to call this function in most cases, because it is called automatically before invoking paintGL().

所以问题似乎是:这里提到的 "other cases" 是什么,它们有什么不同...