17

I would like to use Qt and PyOpenGL to do some real time plotting and learn a bit about OpenGL, but am having trouble even getting my initail test data to show up.

The idea is to store the x-coordinates and y-coordinates in different buffers since the x-coordinates will rarely change while the y-coordintates will change nearly every render. Unfortunately, having them in different buffers is giving me problems.

Right now I have no errors and nothing is showing up so I'm not sure where to go.

Here is what I have so far for the plotting class:

class GLWidget(QtOpenGL.QGLWidget):
    def __init__(self, parent=None):
        self.parent = parent
        QtOpenGL.QGLWidget.__init__(self, parent)
        self.current_position = 0

    def initializeGL(self):
        self.initGeometry()

        self.vertex_code = """
            #version 120
            attribute vec4 color;
            attribute float x_position;
            attribute float y_position;
            varying vec4 v_color;
            void main()
            {
                gl_Position = vec4(x_position, y_position, 0.0, 1.0);
                v_color = color;
            } """

        self.fragment_code = """
            #version 120
            varying vec4 v_color;
            void main()
            {
                //gl_FragColor = v_color;
                gl_FragColor = vec4(1,1,1,1);
            } """

        ## Build and activate program
        # Request program and shader slots from GPU
        self.program = GL.glCreateProgram()
        self.vertex = GL.glCreateShader(GL.GL_VERTEX_SHADER)
        self.fragment = GL.glCreateShader(GL.GL_FRAGMENT_SHADER)

        # Set shaders source
        GL.glShaderSource(self.vertex, self.vertex_code)
        GL.glShaderSource(self.fragment, self.fragment_code)

        # Compile shaders
        GL.glCompileShader(self.vertex)
        GL.glCompileShader(self.fragment)

        # Attach shader objects to the program
        GL.glAttachShader(self.program, self.vertex)
        GL.glAttachShader(self.program, self.fragment)

        # Build program
        GL.glLinkProgram(self.program)

        # Get rid of shaders (not needed anymore)
        GL.glDetachShader(self.program, self.vertex)
        GL.glDetachShader(self.program, self.fragment)

        # Make program the default program
        GL.glUseProgram(self.program)

        # Create array object
        self.vao = GL.glGenVertexArrays(1)
        GL.glBindVertexArray(self.vao)

        # Request buffer slot from GPU
        self.x_data_buffer = GL.glGenBuffers(1)
        GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.x_data_buffer)
        GL.glBufferData(GL.GL_ARRAY_BUFFER, ArrayDatatype.arrayByteCount(self.x), self.x, GL.GL_DYNAMIC_DRAW)

        self.y_data_buffer = GL.glGenBuffers(1)
        GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.y_data_buffer)
        GL.glBufferData(GL.GL_ARRAY_BUFFER, ArrayDatatype.arrayByteCount(self.y), self.y, GL.GL_DYNAMIC_DRAW)




        ## Bind attributes
        #self.stride = self.x.strides[0]
        #self.offset = ctypes.c_void_p(0)
        self.loc = GL.glGetAttribLocation(self.program, "x_position".encode('utf-8'))
        GL.glEnableVertexAttribArray(self.loc)
        GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.x_data_buffer)
        GL.glVertexAttribPointer(self.loc, 1, GL.GL_FLOAT, GL.GL_FALSE, 0, 0)

        #self.stride = self.y.strides[0]
        #self.offset = ctypes.c_void_p(0)
        self.loc = GL.glGetAttribLocation(self.program, "y_position".encode('utf-8'))
        GL.glEnableVertexAttribArray(self.loc)
        GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.y_data_buffer)
        GL.glVertexAttribPointer(self.loc, 1, GL.GL_FLOAT, GL.GL_FALSE, 0, 0)


    def resizeGL(self, width, height):
        if height == 0: height = 1

        GL.glViewport(0, 0, width, height)
        GL.glMatrixMode(GL.GL_PROJECTION)
        GL.glLoadIdentity()
        aspect = width / float(height)

        GLU.gluPerspective(45.0, aspect, 1.0, 100.0)
        GL.glMatrixMode(GL.GL_MODELVIEW)

    def paintGL(self):
        GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)

        GL.glDrawArrays(GL.GL_LINE_STRIP, 0, self.bins)


    def initGeometry(self):
        self.bins = 1000

        self.x = np.linspace(-0.5,0.5,self.bins)
        self.y = np.array([np.sin(val*2*np.pi) for val in self.x])
        self.color = np.array([(1,1,1,1) for x in np.arange(self.bins)])


    def addPoint(self, point):
        #print('ADD POINT')
        self.y[self.current_position] = point
        if self.current_position < self.bins-1:
            self.current_position += 1
        else:
            self.current_position = 0

        return True

    def render(self):
        #print('RENDER')
        self.updateGL()
        return True

I also put a full working version of the program here:

import sys
from PyQt5.QtWidgets import QDesktopWidget, QMainWindow, QWidget, QAction, qApp, QApplication, QHBoxLayout, QVBoxLayout, QPushButton
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QIcon
from PyQt5 import QtOpenGL

from OpenGL import GL
from OpenGL import GLU
from OpenGL.arrays.arraydatatype import ArrayDatatype

import ctypes
import numpy as np
from threading import Timer, Thread, Event

class OxySensor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.initActions()
        self.initMenuBar()
        self.initRenderTimer()
        self.start()

    def initUI(self):
        self.resize(800,600)
        self.center()
        self.setWindowTitle('OxySensor')

        okButton = QPushButton("OK")
        cancelButton = QPushButton("Cancel")


        hbox = QHBoxLayout()
        hbox.addStretch(1)
        hbox.addWidget(okButton)
        hbox.addWidget(cancelButton)

        vbox = QVBoxLayout()
        #vbox.addStretch(1)
        self.gl_widget = GLWidget()
        vbox.addWidget(self.gl_widget)
        vbox.addLayout(hbox)

        mainWidget = QWidget(self)
        mainWidget.setLayout(vbox)

        self.setCentralWidget(mainWidget)


        self.show()

    def initActions(self):
        self.exitAction = QAction(QIcon('images/close20.png'), '&Exit', self)
        self.exitAction.setShortcut('Ctrl+W')
        self.exitAction.setStatusTip('Exit application')
        self.exitAction.triggered.connect(self.onExit)

    def initMenuBar(self):
        menubar = self.menuBar()
        fileMenu = menubar.addMenu('&File')
        fileMenu.addAction(self.exitAction)
        return True

    def initRenderTimer(self):
        self.timer = QTimer()
        self.timer.timeout.connect(self.gl_widget.render)
        self.timer.start(100)
        return True

    def start(self):
        self.stop_flag = Event()
        self.thread = SerialPort(self.onTimerExpired, self.stop_flag)
        self.thread.start()
        self.statusBar().showMessage('Ready')

    def center(self):
        qr = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())
        return True

    def onTimerExpired(self):
        data = np.random.uniform(-1,1)
        self.gl_widget.addPoint(data)
        return True

    def onExit(self):
        self.close()
        return None

    def closeEvent(self,event):
        self.stop_flag.set()
        event.accept()
        return None

class GLWidget(QtOpenGL.QGLWidget):
    def __init__(self, parent=None):
        self.parent = parent
        QtOpenGL.QGLWidget.__init__(self, parent)
        self.yRotDeg = 0.0
        self.current_position = 0

    def initializeGL(self):
        self.initGeometry()

        self.vertex_code = """
            #version 120
            attribute vec4 color;
            attribute float x_position;
            attribute float y_position;
            varying vec4 v_color;
            void main()
            {
                gl_Position = vec4(x_position, y_position, 0.0, 1.0);
                v_color = color;
            } """

        self.fragment_code = """
            #version 120
            varying vec4 v_color;
            void main()
            {
                //gl_FragColor = v_color;
                gl_FragColor = vec4(1,1,1,1);
            } """

        ## Build and activate program
        # Request program and shader slots from GPU
        self.program = GL.glCreateProgram()
        self.vertex = GL.glCreateShader(GL.GL_VERTEX_SHADER)
        self.fragment = GL.glCreateShader(GL.GL_FRAGMENT_SHADER)

        # Set shaders source
        GL.glShaderSource(self.vertex, self.vertex_code)
        GL.glShaderSource(self.fragment, self.fragment_code)

        # Compile shaders
        GL.glCompileShader(self.vertex)
        GL.glCompileShader(self.fragment)

        # Attach shader objects to the program
        GL.glAttachShader(self.program, self.vertex)
        GL.glAttachShader(self.program, self.fragment)

        # Build program
        GL.glLinkProgram(self.program)

        # Get rid of shaders (not needed anymore)
        GL.glDetachShader(self.program, self.vertex)
        GL.glDetachShader(self.program, self.fragment)

        # Make program the default program
        GL.glUseProgram(self.program)

        # Create array object
        self.vao = GL.glGenVertexArrays(1)
        GL.glBindVertexArray(self.vao)

        # Request buffer slot from GPU
        self.x_data_buffer = GL.glGenBuffers(1)
        GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.x_data_buffer)
        GL.glBufferData(GL.GL_ARRAY_BUFFER, ArrayDatatype.arrayByteCount(self.x), self.x, GL.GL_DYNAMIC_DRAW)

        self.y_data_buffer = GL.glGenBuffers(1)
        GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.y_data_buffer)
        GL.glBufferData(GL.GL_ARRAY_BUFFER, ArrayDatatype.arrayByteCount(self.y), self.y, GL.GL_DYNAMIC_DRAW)




        ## Bind attributes
        #self.stride = self.x.strides[0]
        #self.offset = ctypes.c_void_p(0)
        self.loc = GL.glGetAttribLocation(self.program, "x_position".encode('utf-8'))
        GL.glEnableVertexAttribArray(self.loc)
        GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.x_data_buffer)
        GL.glVertexAttribPointer(self.loc, 1, GL.GL_FLOAT, GL.GL_FALSE, 0, 0)

        #self.stride = self.y.strides[0]
        #self.offset = ctypes.c_void_p(0)
        self.loc = GL.glGetAttribLocation(self.program, "y_position".encode('utf-8'))
        GL.glEnableVertexAttribArray(self.loc)
        GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.y_data_buffer)
        GL.glVertexAttribPointer(self.loc, 1, GL.GL_FLOAT, GL.GL_FALSE, 0, 0)


    def resizeGL(self, width, height):
        if height == 0: height = 1

        GL.glViewport(0, 0, width, height)
        GL.glMatrixMode(GL.GL_PROJECTION)
        GL.glLoadIdentity()
        aspect = width / float(height)

        GLU.gluPerspective(45.0, aspect, 1.0, 100.0)
        GL.glMatrixMode(GL.GL_MODELVIEW)

    def paintGL(self):
        GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)

        GL.glDrawArrays(GL.GL_LINE_STRIP, 0, self.bins)


    def initGeometry(self):
        self.bins = 1000

        self.x = np.linspace(-0.5,0.5,self.bins)
        self.y = np.array([np.sin(val*2*np.pi) for val in self.x])
        self.color = np.array([(1,1,1,1) for x in np.arange(self.bins)])


    def addPoint(self, point):
        #print('ADD POINT')
        self.y[self.current_position] = point
        if self.current_position < self.bins-1:
            self.current_position += 1
        else:
            self.current_position = 0

        return True

    def render(self):
        #print('RENDER')
        self.updateGL()
        return True

class SerialPort(Thread):
    def __init__(self, callback, event):
        Thread.__init__(self)
        self.callback = callback
        self.stopped = event
        return None

    def SetInterval(self, time_in_seconds):
        self.delay_period = time_in_seconds
        return True

    def run(self):
        while not self.stopped.wait(0.1):
            self.callback()
        return True

if __name__ == '__main__':
    app = QApplication(sys.argv)
    oxy_sensor = OxySensor()
    sys.exit(app.exec_())
genpfault
  • 51,148
  • 11
  • 85
  • 139
user2027202827
  • 1,234
  • 11
  • 29
  • Where is the `updateGL()` method? The code posted so far does never actually update the VBO contents after initally creating them. – derhass Nov 03 '15 at 19:45
  • `updateGL()` is in the `render()` method – user2027202827 Nov 03 '15 at 19:49
  • Well, I can see that it is _called_ there. Maybe I'm misunderstanding something, and this `updateGL` is some method of the pthon OpenGL widget. If so, then my comment applies even more, though: you never actually update those VBOs then (which I'd assumed to be done in `updateGL`). – derhass Nov 03 '15 at 19:52
  • Yeah I haven't written that part yet but even so, shouldn't the initial data I loaded into the buffers be plotted? – user2027202827 Nov 03 '15 at 20:07
  • Yeah, it probably should. Your GL code looks good so far. Something which comes to mind: you seem to be using a legacy GL context (GLSL 1,20, usage of bogus `gluPerspective`) and might hit a weird scenario: In such a context, drawing will only work if the attribute array for attribute 0 is actually enabled. Now you have that currently unused `color` attribute, which might have been assigned attribute 0... I'd recommend using a modern GL >=3.2 core profile context. If that is not possible, I'd recommend uing `glBindAttribLocation()` to explicitely assign the locations and to use location 0. – derhass Nov 03 '15 at 20:51
  • Yeah unfortunately my computer only supports up to opengl 2. You were correct that x_position and y_position were getting ids of 1 and 2 respectively. I tried binding them explicitely to 0 and 1 as you suggested but still get a blank screen. I also removed the call to gluPerspective since that's actually left over from the example I was working from as a starting point (a rotating cube that put all verticies in one buffer). – user2027202827 Nov 03 '15 at 21:12
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/94138/discussion-between-derhass-and-hobenkr). – derhass Nov 03 '15 at 21:15

2 Answers2

3

The problem is most likely that call of the 'OpenGL.GL' function:

GL.glVertexAttribPointer(self.loc, 1, GL.GL_FLOAT, GL.GL_FALSE, 0, 0)

Note the last value is 0 (which unfortunately doesn't translate to (void *)0 in the C++ API, but instead to some high address). You most likely mean "offset 0" as opposed to "the address of the 0 object". I. e. use None instead (which does translate to (void *)0):

GL.glVertexAttribPointer(self.loc, 1, GL.GL_FLOAT, GL.GL_FALSE, 0, None)

Yes, it's kinda counterintuitive. It took me half a day to figure out that's what made the output black in my own code.

Also note that if instead of the OpenGL.GL functions you use the Qt ones (that you get via gl = self.context().versionFunctions()), they provide a slightly different API and there you actually pass 0 when you mean "Offset 0".

blubberdiblub
  • 4,085
  • 1
  • 28
  • 30
  • Hey, I haven't been on this site it a while but just wanted to thank you for that briliant catch passing `None` to `GL.glVertexAttribPointer()` :) Thanks for sharing. – user2027202827 Jun 10 '16 at 21:18
-2

You should take a look at the program Avogadro
http://avogadro.cc/wiki/Main_Page which is based on OpenGL functions.

The source code is free and it may be useful.

  • 1
    A link to a potential solution is always welcome, but please add context around the link so your fellow users will have some idea what it is and why it’s there. Always quote the most relevant part of an important link, in case the target site is unreachable or goes permanently offline. – Tom Sabel Apr 14 '16 at 08:09
  • ...especially since the link is now broken! – uhoh Sep 27 '17 at 06:51