Search code examples
pythonopenglpyqt5pyopenglopengl-compat

Why doesn't update repaint a scene?


I am making a marching cubes project in python using PyQt5 and PyOpenGL. I am trying to hide the wireframe cube which marches across the screen, referenced as mainWindow.marchingCube to disappear after cycling through. I managed to get the disappearing cycle to occur, but the cube does not actually disappear. I called the QOpenGLWidget's update function, but the cube still did not disappear.

import sys
from PyQt5.QtWidgets import (
                             QApplication, QMainWindow, QSlider,
                             QOpenGLWidget, QLabel, QPushButton
                            )
from PyQt5.QtCore import Qt
from OpenGL.GL import (
                       glLoadIdentity, glTranslatef, glRotatef,
                       glClear, glBegin, glEnd,
                       glColor3fv, glVertex3fv,
                       GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT,
                       GL_QUADS, GL_LINES
                      )
from OpenGL.GLU import gluPerspective
from numerics import sin, cos, tan, avg, rnd    #Numerics is my custom math library.
import random, time

class mainWindow(QMainWindow):    #Main class.
    shapes = []    #this will hold instances of the following classes: cube
    dataPoints = []
    zoomLevel = -10
    rotateDegreeV = -90
    rotateDegreeH = 0
    marchActive = False
    limit = -1
    meshPoints = []

    class cube():
        render = True
        solid = False
        color = (1, 1, 1)

        def config(self, x, y, z, size = 0.1, solid = False, color = (1, 1, 1)):
            self.solid = solid
            self.color = color
            self.size = size / 2
            s = self.size
            self.vertices = [
                    (-s + x, s + y, -s + z),
                    (s + x, s + y, -s + z),
                    (s + x, -s + y, -s + z),
                    (-s + x, -s + y, -s + z),
                    (-s + x, s + y, s + z),
                    (s + x, s + y, s + z),
                    (s + x, -s + y, s + z),
                    (-s + x, -s + y, s + z)
                   ]
            self.edges = [
                          (0,1), (0,3), (0,4), (2,1),
                          (2,3), (2,6), (7,3), (7,4),
                          (7,6), (5,1), (5,4), (5,6)
                         ]
            self.facets = [
                           (0, 1, 2, 3), (0, 1, 6, 5),
                           (0, 3, 7, 4), (6, 5, 1, 2),
                           (6, 7, 4, 5), (6, 7, 3, 2)
                          ]
        def show(self):
            self.render = True
        def hide(self):
            self.render = False

    class dataPoint():
        location = (0, 0, 0)
        value = 0
        shape = None
        def place(self, x, y, z):
            self.location = (x, y, z)
        def set(self, val):
            self.value = val
        def setShape(self, shape):
            self.shape = shape

    class meshPoint():
        location = (0, 0, 0)
        shape = None
        def place(self, x, y, z):
            self.location = (x, y, z)
        def setShape(self, shape):
            self.shape = shape

    def keyPressEvent(self, event):    #This is the keypress detector. I use this to determine input to edit grids.
        try:
            key = event.key()
            #print(key)
            if key == 87:
                self.rotateV(5)
            elif key == 65:
                self.rotateH(5)
            elif key == 83:
                self.rotateV(-5)
            elif key == 68:
                self.rotateH(-5)
            elif key == 67:
                self.zoom(1)
            elif key == 88:
                self.zoom(-1)
            elif key == 77:
                self.marchStep()
        except:
            pass

    def __init__(self):
        super(mainWindow, self).__init__()
        self.currentStep = 0
        self.width = 700    #Variables used for the setting of the size of everything
        self.height = 600
        self.setGeometry(0, 0, self.width + 50, self.height)    #Set the window size
        self.initData(3, 3, 3)
    def setupUI(self):
        self.openGLWidget = QOpenGLWidget(self)    #Create the GLWidget
        self.openGLWidget.setGeometry(0, 0, self.width, self.height)
        self.openGLWidget.initializeGL()
        self.openGLWidget.resizeGL(self.width, self.height)    #Resize GL's knowledge of the window to match the physical size?
        self.openGLWidget.paintGL = self.paintGL    #override the default function with my own?

        self.filterSlider = QSlider(Qt.Vertical, self)
        self.filterSlider.setGeometry(self.width + 10, int(self.height / 2) - 100, 30, 200)
        self.filterSlider.valueChanged[int].connect(self.filter)

        self.limitDisplay = QLabel(self)
        self.limitDisplay.setGeometry(self.width, int(self.height / 2) - 130, 50, 30)
        self.limitDisplay.setAlignment(Qt.AlignCenter)
        self.limitDisplay.setText('-1')

        self.marchButton = QPushButton(self)
        self.marchButton.setGeometry(self.width, int(self.height / 2) - 160, 50, 30)
        self.marchButton.setText('March!')
        self.marchButton.clicked.connect(self.marchStep)

    def marchStep(self):
        if not self.marchActive:
            marchAddr = len(self.shapes)
            self.shapes.append(self.cube())
            self.marchingCube = self.shapes[marchAddr]
            self.marchActive = True
            self.currentStep = 0
        if self.currentStep == len(self.marchPoints):
            self.currentStep = 0
            #print('meshPoints: {}'.format(self.meshPoints))
            for mp in self.meshPoints:
                #print(mp.shape)
                self.shapes.remove(mp.shape)
            self.meshPoints.clear()
            self.marchingCube.hide()
            return

        if self.currentStep == 0:
            self.marchingCube.show()

        p = self.marchPoints[self.currentStep]
        x, y, z = p
        self.marchingCube.config(x, y, z, size = 1)
        points = []
        for i in range(8):
            point = self.getDataPointByLocation(self.marchingCube.vertices[i])
            points.append(point)
        self.openGLWidget.update()
        #print('step: {}  x: {}  y: {}  z: {}'.format(self.currentStep, x, y, z))
        #for point in points:
        #    print(point.location, end = ' ')
        #print()
        for pair in self.marchingCube.edges:
            pointA = points[pair[0]]
            pointB = points[pair[1]]
            #print('pointA.value: {}  pointB.value: {}  limit: {}'.formatpointA.value, pointB.value, self.limit)
            if (pointA.value < self.limit and pointB.value > self.limit) or (pointA.value > self.limit and pointB.value < self.limit):
                xA, yA, zA = pointA.location
                xB, yB, zB = pointB.location
                valA = (pointA.value + 1) / 2
                valB = (pointB.value + 1) / 2
                xC = float(avg([xA, xB]))
                yC = float(avg([yA, yB]))
                zC = float(avg([zA, zB]))

                mp = self.meshPoint()
                mp.place(xC, yC, zC)
                mp.setShape(self.cube())
                mp.shape.config(xC, yC, zC, size = 0.05, solid = True, color = (1, 0, 0))
                self.shapes.append(mp.shape)
                self.meshPoints.append(mp)
        self.currentStep += 1
        self.openGLWidget.update()

    def zoom(self, value):
        self.zoomLevel += value
        self.openGLWidget.update()

    def rotateV(self, value):
        self.rotateDegreeV += value
        self.openGLWidget.update()

    def rotateH(self, value):
        self.rotateDegreeH += value
        self.openGLWidget.update()

    def filter(self, value):
        self.limit = rnd((value / 49.5) -1, -2)
        for d in self.dataPoints:
            if d.value < self.limit:
                d.shape.hide()
            else:
                d.shape.show()
        self.limitDisplay.setText(str(self.limit))
        self.openGLWidget.update()

    def getDataPointByLocation(self, coord):
        x, y, z = coord
        for dp in self.dataPoints:
            if dp.location == (x, y, z):
                return dp
        return False

    def paintGL(self):
        glLoadIdentity()
        gluPerspective(45, self.width / self.height, 0.1, 110.0)    #set perspective?
        glTranslatef(0, 0, self.zoomLevel)    #I used -10 instead of -2 in the PyGame version.
        glRotatef(self.rotateDegreeV, 1, 0, 0)    #I used 2 instead of 1 in the PyGame version.
        glRotatef(self.rotateDegreeH, 0, 0, 1)
        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

        if len(self.shapes) != 0:
            glBegin(GL_LINES)
            for s in self.shapes:
                glColor3fv(s.color)
                if s.render and not s.solid:
                    for e in s.edges:
                        for v in e:
                            glVertex3fv(s.vertices[v])
            glEnd()

            glBegin(GL_QUADS)
            for s in self.shapes:
                glColor3fv(s.color)
                if s.render and s.solid:
                    for f in s.facets:
                        for v in f:
                            glVertex3fv(s.vertices[v])
            glEnd()

    def initData(self, sizeX, sizeY, sizeZ):
        marchSizeX = sizeX - 1
        marchSizeY = sizeY - 1
        marchSizeZ = sizeZ - 1
        xOff = -(sizeX / 2) + 0.5
        yOff = -(sizeY / 2) + 0.5
        zOff = -(sizeZ / 2) + 0.5
        xMarchOff = -(marchSizeX / 2) + 0.5
        yMarchOff = -(marchSizeY / 2) + 0.5
        zMarchOff = -(marchSizeZ / 2) + 0.5
        vals = []
        self.marchPoints = []
        for z in range(marchSizeZ):
            for y in range(marchSizeY):
                for x in range(marchSizeX):
                    self.marchPoints.append((x + xMarchOff, y + yMarchOff ,z + zMarchOff))

        for z in range(sizeZ):
            for y in range(sizeY):
                for x in range(sizeX):
                    loc = len(self.dataPoints)
                    val = self.generate(x + xOff, y + yOff, z + zOff)
                    self.dataPoints.append(self.dataPoint())
                    self.dataPoints[loc].place(x + xOff, y + yOff, z + zOff)
                    self.dataPoints[loc].set(val)
                    loc2 = len(self.shapes)
                    self.shapes.append(self.cube())
                    self.shapes[loc2].config(x + xOff, y + yOff, z + zOff, solid = True, color = (0, (val + 1) / 2, (val + 1) / -2 + 1))
                    self.dataPoints[loc].setShape(self.shapes[loc2])
                    vals.append(val)
        print(avg(vals))

    def generate(self, xIn, yIn, zIn):    #Function which produces semi-random values based on the supplied coordinates.
        i = -(xIn * yIn * (10 + zIn))
        j = xIn * yIn * (10 + zIn)
        if i < j:
            mixer = random.randint(i, j)
        else:
            mixer = random.randint(j, i + 1)
        a = avg([sin(cos(xIn)), tan(tan(yIn)), cos(tan(zIn))])
        out = mixer * a
        while out > 10:
            out -= 5
        while out < -10:
            out += 5
        return float(out / 10)

app = QApplication([])
window = mainWindow()
window.setupUI()
window.show()
sys.exit(app.exec_())

Why doesn't the cube disappear? I have caught wind during my web searches on the subject that update does not always work as expected. Directly calling self.openGLWidget.paintGL() does not work either. What must I do to make the cube disappear?

EDIT: If I make a call to rotate, rotate, or zoom, the screen refreshes and the meshpoints as well as the marching cube all disappear. I think I may end up making a workaround by calling one of those with a zero value.

To test, save the following code in a file named numerics.py in the same directory as the rest of the code.

from decimal import Decimal as dec

degrad = 'deg'
pi = 3.14159265358979323846
terms = dec(9)    #number of terms used for the trig calculations

def version():
    print('numerics.py version 1.0.0')
    print('Packaged with the cubes project')
def mode(modeinput = ''):    #switch between degrees and radians or check the current mode
    global degrad
    if modeinput == 'deg':
        degrad = 'deg'
        return 'deg'
    if modeinput == 'rad':
        degrad = 'rad'
        return 'rad'
    if modeinput == '':
        return degrad
    else:
        return False

def accuracy(accinput = ''):
    global terms
    global pi
    if accinput == '':
        return terms
    terms = dec(accinput)
    PI = calculatePi(accinput)
    print('Pi is: {}'.format(PI))
    return terms

def calculatePi(placeIn = terms):
    if placeIn > 15:
        if input("Warning: You have chosen to calculate more than 20 digits of pi. This may take a LONG TIME and may be inacurate. Enter 'yes' if you wish to proceed. If you enter anything else, this function will revert to 10 digits.") == 'yes':
            place = placeIn
        else:
            place = 10
    else:
        place = placeIn
    print('Calculating Pi...\nPlease wait, as this may take a while.')
    PI = dec(3)
    addSub = True
    for i in range(2, 2 * (int(place) ** 6) + 1, 2):
        if addSub:
            PI += dec(4) / (dec(i) * dec(i + 1) * dec(i + 2))
        elif not addSub:
            PI -= dec(4) / (dec(i) * dec(i + 1) * dec(i + 2))
        addSub = not addSub
    return rnd(PI, -(place), mode = 'cutoff')

def radToDeg(radin):
    return (dec(radin) * dec(180 / pi))

def degToRad(degin):
    return (dec(degin) * dec(pi / 180))

def avg(numsIn):    #return the average of two numbers, specified as an integer or float
    num1 = dec(0)
    for i in numsIn:
        num1 += dec(i)
    return rnd(dec(num1 / dec(len(numsIn))))

def sin(anglein, dr = degrad):    #return sine of the supplied angle using the predetermined mode or the supplied mode
    if dr == 'deg':
        while anglein > 180:
            anglein -= 360
        while anglein < -180:
            anglein += 360
        angle = degToRad(anglein)
    if dr == 'rad':
        while anglein > pi:
            anglein -= (2 * pi)
        while anglein < -pi:
            anglein += (2 * pi)
        angle = anglein
    return rnd(rawsin(dec(angle)), -terms)

def arcsin(ratioin, dr = degrad):    #return arcsine of the supplied ratio using the predetermined mode or the supplied mode
    if ratioin > 1 or ratioin < -1:    #if the input is illegal
        return False
    attempt = dec(0)    #start at 0
    target = rnd(dec(ratioin), -terms)    #identify the target value
    #print('target is: {}'.format(target))
    for i in range(-1, int(terms) + 1):    #for each place from 10s to terms decimal place (use -i, not i)
        #print('Editing place {0}'.format(10 ** -i))    #debugging
        for j in range(10):    #for 10 steps
            #print('current attempt: {}'.format(attempt), end = ' ')
            if rnd(sin(attempt, dr), -terms) == target:
                if attempt < 0:
                    final = (attempt * dec(-1))
                else:
                    final = attempt
                #print('attempt: {0} final: {1}'.format(attempt, final))
                return final
            if rnd(sin(attempt, dr), -terms) < target:
                #add some
                attempt += (dec(10) ** -i)
                #print('attempt: {}'.format(attempt), end = ' ')
            if rnd(sin(attempt, dr), -terms) > target:
                #subtract some
                attempt -= (dec(10) ** -i)
                #print('attempt: {}'.format(attempt), end = ' ')
            #print('')
    if attempt < 0:
        final = (attempt * dec(-1))
    else:
        final = attempt
    #print('attempt: {0} final: {1}'.format(attempt, final))
    return (final)

def cos(anglein, dr = degrad):    #return cosine of the supplied angle
    if dr == 'deg':
        return rawsin(degToRad(90 - anglein))
    else:
        angle = anglein
        return rnd(rawsin(90 - angle), -terms)

def arccos(ratioin, dr = degrad):    #return arccosine of the supplied ratio
    if ratioin > 1 or ratioin < -1:
        return False
    attempt = dec(0)    #start at 0
    target = rnd(dec(ratioin), -terms)    #identify the target value
    #print('target is: {}'.format(target))
    for i in range(-1, int(terms) + 1):    #for each place from 10s to terms decimal place (use -i, not i)
        #print('Editing place {0}'.format(10 ** -i))    #debugging
        for j in range(10):    #for 10 steps
            #print('current attempt: {}'.format(attempt), end = ' ')
            if rnd(cos(attempt, dr), -terms) == target:
                if attempt < 0:
                    final = (attempt * dec(-1))
                else:
                    final = attempt
                #print('attempt: {0} final: {1}'.format(attempt, final))
                return final
            if rnd(cos(attempt, dr), -terms) < target:
                #add some
                attempt += (dec(10) ** -i)
                #print('attempt: {}'.format(attempt), end = ' ')
            if rnd(cos(attempt, dr), -terms) > target:
                #subtract some
                attempt -= (dec(10) ** -i)
                #print('attempt: {}'.format(attempt), end = ' ')
            #print('')
    if attempt < 0:
        final = (attempt * dec(-1))
    else:
        final = attempt
    #print('attempt: {0} final: {1}'.format(attempt, final))
    return (final)

def tan(anglein, dr = degrad):    #return tangent of the supplied angle
    a = sin(anglein, dr)
    b = cos(anglein, dr)
    if (not a == 0) and (not b == 0):
        return rnd((a / b), -terms)
    else:
        return False

def arctan(ratioin, dr = degrad):    #return arctangent of the supplied ratio
    if ratioin > 1 or ratioin < -1:
        return False
    attempt = dec(0)    #start at 0
    target = rnd(dec(ratioin), -terms)    #identify the target value
    #print('target is: {}'.format(target))
    for i in range(-1, int(terms) + 1):    #for each place from 10s to terms decimal place (use -i, not i)
        #print('Editing place {0}'.format(10 ** -i))    #debugging
        for j in range(10):    #for 10 steps
            #print('current attempt: {}'.format(attempt), end = ' ')
            if rnd(tan(attempt, dr), -terms) == target:
                if attempt < 0:
                    final = (attempt * dec(-1))
                else:
                    final = attempt
                #print('attempt: {0} final: {1}'.format(attempt, final))
                return final
            if rnd(tan(attempt, dr), -terms) < target:
                #add some
                attempt += (dec(10) ** -i)
                #print('attempt: {}'.format(attempt), end = ' ')
            if rnd(tan(attempt, dr), -terms) > target:
                #subtract some
                attempt -= (dec(10) ** -i)
                #print('attempt: {}'.format(attempt), end = ' ')
            #print('')
    if attempt < 0:
        final = (attempt * dec(-1))
    else:
        final = attempt
    #print('attempt: {0} final: {1}'.format(attempt, final))
    return (final)

def rawsin(anglein):    #return the result of sine of the supplied angle, using radians
#This is the taylor series used.
#final = x - (x^3 / 3!) + (x^5 / 5!) - (x^7 / 7!) + (x^9 / 9!) - (x^11 / 11!)...
    angle = dec(anglein)
    final = angle
    add = False
    for i in range(3, int(terms) * 3, 2):
        if add:
            final += dec(angle ** i) / fact(i)
        elif not add:
            final -= dec(angle ** i) / fact(i)
        add = not add
    return final

def fact(intin):    #return the factorial of the given integer, return False if not given an int
    if intin == int(intin):
        intout = 1
        for i in range(1, intin + 1):
            intout *= i
        return intout
    else:
        return False

def rnd(numIn, decPlcIn = -terms, mode = 'fiveHigher'):    #return the given number, rounded to the given decimal place.
#use 1 to indicate 10s, 0 to indicate 1s, -2 to indicate 100ths, etc.
    num1 = dec(numIn)
    decPlc = dec(decPlcIn)
    if mode == 'fiveHigher':
        return dec(str(dec(round(num1 * (dec(10) ** -decPlc))) * (dec(10) ** decPlc)).rstrip('0'))
    elif mode == 'cutoff':
        return dec(str(dec(int(num1 * (dec(10) ** -decPlc))) * (dec(10) ** decPlc)).rstrip('0'))

def root(numIn, rootVal):
    num = dec(numIn)
    rt = dec(dec(1) / rootVal)
    num1 = num ** rt
    return rnd(num1, -terms)

def quad(aIn, bIn, cIn):    #Plugin for the quadratic formula. Provide a, b, and c.
    a = dec(aIn)
    b = dec(bIn)
    c = dec(cIn)
    try:
        posResult = (-b + root((b ** dec(2)) - (dec(4) * a * c), 2)) / (dec(2) * a)
    except:
        posResult = False
    try:
        negResult = (-b - root((b ** dec(2)) - (dec(4) * a * c), 2)) / (dec(2) * a)
    except:
        negResult = False
    return (posResult, negResult)

Solution

  • You are missing 1 call to self.openGLWidget.update(). There is a return statement in the instruction block of the if. The function is terminated at this point and the self.openGLWidget.update() instruction at the end of the code is never executed.

    Add self.openGLWidget.update() right before return, to solve the issue:

    class mainWindow(QMainWindow):  
        # [...]
    
        def marchStep(self):
            if not self.marchActive:
                # [...]
                self.currentStep = 0
            if self.currentStep == len(self.marchPoints):
                # [...]
                self.meshPoints.clear()
                self.marchingCube.hide()
    
                self.openGLWidget.update() # <--------- ADD 
                return
    
            if self.currentStep == 0:
                self.marchingCube.show()
    
            # [...]