Search code examples
pythonopenglpyqt5pyopengl

PyOpenGL how to rebind VAO to another VBO in order to quickly switch data that needs to be drawn


I am using PyQt5 QOpenGLWidget to simply draw some points on the screen with different colors. I prepared two datasets, each containing their coordinates and their colors, 4 arrays on total, and I want the program to paint one of the datasets for each 100ms (by using a QTimer). I put these 4 arrays into 4 VBOs, and for each paintGL() call I want to switch from a dataset to another, that is, if this frame uses VBO0(coordinate), VBO1(color), then next frame will use VBO2(coordinate), VBO3(color), and vice versa.

My idea is to use a VAO, and since VBO0/1 has exactly same structure with VBO2/3, for each new frame I can just rebind the VAO to another VBO. However this does not work as expected, so what is the correct way of achieving my goal?

Minimum Example:

import numpy as np
from PyQt5.QtWidgets import QOpenGLWidget, QApplication, QWidget, QHBoxLayout
from PyQt5.QtCore import QTimer

from OpenGL.GL import *


def compileShader(vsSource, fsSource):

    vertexShader = glCreateShader(GL_VERTEX_SHADER)
    glShaderSource(vertexShader, vsSource)
    glCompileShader(vertexShader)

    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER)
    glShaderSource(fragmentShader, fsSource)
    glCompileShader(fragmentShader)

    program = glCreateProgram()
    glAttachShader(program, vertexShader)
    glAttachShader(program, fragmentShader)
    glLinkProgram(program)

    glDeleteShader(vertexShader)
    glDeleteShader(fragmentShader)

    return program


class MyOpenGLWidget(QOpenGLWidget):

    def __init__(self):

        super().__init__()

   


    def initializeGL(self):

        # with open('./shaders/vertexShader.shader', 'r') as f:
        #     vsSource = f.read()
        # with open('./shaders/fragmentShader.shader', 'r') as f:
        #     fsSource = f.read()

        vsSource = """
        #version 450 core
        layout (location = 0) in vec4 position;
        layout (location = 1) in vec4 color;
        
        out vec4 vs_Color;
        
        void main() {
        
            gl_Position = position;
            gl_PointSize = 5.;
            vs_Color = color;
        
        }
        """

        fsSource = """
        #version 450 core
        out vec4 FragColor;

        in vec4 vs_Color;
        
        void main() {
        
            FragColor = vs_Color;
        
        }
        """

        self.program = compileShader(vsSource, fsSource)
        self.initBuffers()
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.onTimeout)
        self.timer.start(100)
        self.dataset = 1


    def paintGL(self):

        bgColor = np.array([0, 0, 0, 1], dtype=np.float32)
        glClearBufferfv(GL_COLOR, 0, bgColor)
        glUseProgram(self.program)
        glEnable(GL_PROGRAM_POINT_SIZE)
        glDrawArrays(GL_POINTS, 0, self.n)


    def generateRandomData(self, n, mode='vertex'):

        if mode == 'vertex':
            dx = np.random.rand(n) * 2 - 1
            dy = np.random.rand(n) * 2 - 1
            zeros = np.zeros(n)
            ones = np.ones(n)
            data = np.vstack([dx, dy, zeros, ones]).T.astype(np.float32)
            return data.flatten()

        elif mode == 'color':
            r = np.random.rand(n)
            g = np.random.rand(n)
            b = np.random.rand(n)
            ones = np.ones(n)
            data = np.vstack([r, g, b, ones]).T.astype(np.float32)
            return data.flatten()


    def initBuffers(self):

        self.n = 100
        self.vertexData1 = self.generateRandomData(self.n, mode='vertex')
        self.colorData1 = self.generateRandomData(self.n, mode='color')
        self.vertexData2 = self.generateRandomData(self.n, mode='vertex')
        self.colorData2 = self.generateRandomData(self.n, mode='color')

        self.buffers = np.empty(4, dtype=np.uint32)
        glCreateBuffers(4, self.buffers)

        for buffer in self.buffers:
            glBindBuffer(GL_ARRAY_BUFFER, buffer)
            glNamedBufferStorage(buffer, self.vertexData1.nbytes, None,
                                 GL_DYNAMIC_STORAGE_BIT)

        glNamedBufferSubData(self.buffers[0], 0, self.vertexData1.nbytes, self.vertexData1)
        glNamedBufferSubData(self.buffers[1], 0, self.colorData1.nbytes, self.colorData1)
        glNamedBufferSubData(self.buffers[2], 0, self.vertexData2.nbytes, self.vertexData2)
        glNamedBufferSubData(self.buffers[3], 0, self.colorData2.nbytes, self.colorData2)

        self.VAO = GLuint(0)
        glCreateVertexArrays(1, self.VAO)

        glEnableVertexArrayAttrib(self.VAO, 0)
        glEnableVertexArrayAttrib(self.VAO, 1)
        glVertexArrayAttribFormat(self.VAO, 0, 4, GL_FLOAT, GL_FALSE, 0)
        glVertexArrayAttribFormat(self.VAO, 1, 4, GL_FLOAT, GL_FALSE, 0)
        glVertexArrayAttribBinding(self.VAO, 0, 0)
        glVertexArrayAttribBinding(self.VAO, 1, 1)

        glVertexArrayVertexBuffer(self.VAO, 0, self.buffers[0], 0, 4 * 4)
        glVertexArrayVertexBuffer(self.VAO, 1, self.buffers[1], 0, 4 * 4)


        glBindVertexArray(self.VAO)


    def onTimeout(self):

        if self.dataset == 1:
            glVertexArrayVertexBuffer(self.VAO, 0, self.buffers[2], 0, 4 * 4)
            glVertexArrayVertexBuffer(self.VAO, 1, self.buffers[3], 0, 4 * 4)
            self.dataset = 2
        elif self.dataset == 2:
            glVertexArrayVertexBuffer(self.VAO, 0, self.buffers[0], 0, 4 * 4)
            glVertexArrayVertexBuffer(self.VAO, 1, self.buffers[1], 0, 4 * 4)
            self.dataset = 1
        self.update()


app = QApplication([])
win = QWidget()
win.showMaximized()
layout = QHBoxLayout()
win.setLayout(layout)
layout.addWidget(MyOpenGLWidget())
win.show()
app.exec_()

Expected Output:

Two datsets switches on the screen every 100ms (as set in QTimer) like: enter image description here

Real Output:

First frame is correct, but after first switching there is a white triangle on the screen instead of points, and there's no further effect. enter image description here


Solution

  • Rebind your vertex array object (self.VAO) each time in paintGL rather than just once in initBuffers. The official doco is not very clear, but these earlier answers suggest that changing attribs on a VAO probably won't take effect on the current binding.

    When is What bound to a VAO?

    Changing vertex array object