Source code for Elements.pyGLV.GUI.Viewer

"""
Viewer classes, part of the Elements.pyGLV
    
Elements.pyGLV (Computer Graphics for Deep Learning and Scientific Visualization)
@Copyright 2021-2022 Dr. George Papagiannakis
    
The classes below are all related to the GUI and Display part of the package

Basic design principles are based on the Decorator Design pattern:
	• https://refactoring.guru/design-patterns/decorator
	• https://github.com/faif/python-patterns/blob/master/patterns/structural/decorator.py
"""

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List, Dict, Any
from collections.abc import Iterable, Iterator
from sys import platform

import sdl2
import sdl2.ext
from sdl2.keycode import SDLK_ESCAPE,SDLK_w
from sdl2.video import SDL_WINDOWPOS_CENTERED, SDL_WINDOW_ALLOW_HIGHDPI
import OpenGL.GL as gl
from OpenGL.GL import shaders
import imgui
from imgui.integrations.sdl2 import SDL2Renderer

import Elements.pyECSS.System    
import Elements.pyECSS.math_utilities as util
import Elements.pyECSS.Event
from Elements.pyECSS.System import System  
from Elements.pyECSS.Component import BasicTransform
import numpy as np
from scipy.spatial.transform import Rotation
# from Elements.pyGLV.GL.Scene import Scene
# from Elements.pyGLV.GL.Scene import Scene

import Elements.utils.Shortcuts as Shortcuts

[docs]class RenderWindow(ABC): """ The Abstract base class of the Viewer GUI/Display sub-system of pyglGA based on the Decorator Pattern, this class is "wrapped" by decorators in order to provide extra cpapabilities e.g. SDL2 window, context and ImGUI widgets """
[docs] def __init__(self): self._eventManager = None self._scene = None
#define properties for EventManager, Scene objects @property #name def eventManager(self): """ Get RenderWindow's eventManager """ return self._eventManager @eventManager.setter def eventManager(self, value): self._eventManager = value @property #name def scene(self): """ Get RenderWindow's Scene reference """ return self._scene @scene.setter def scene(self, value): self._scene = value @abstractmethod def init(self): raise NotImplementedError abstractmethod def init_post(self): raise NotImplementedError @abstractmethod def display(self): raise NotImplementedError @abstractmethod def display_post(self): raise NotImplementedError @abstractmethod def shutdown(self): raise NotImplementedError @abstractmethod def event_input_process(self, running = True): raise NotImplementedError @abstractmethod def accept(self, system: Elements.pyECSS.System, event = None): """ Accepts a class object to operate on the RenderWindow, based on the Visitor pattern. :param system: [a System object] :type system: [System] """ raise NotImplementedError @classmethod def getClassName(cls): return cls.__name__
[docs]class SDL2Window(RenderWindow): """ The concrete subclass of RenderWindow for the SDL2 GUI API :param RenderWindow: [description] :type RenderWindow: [type] """
[docs] def __init__(self, windowWidth = None, windowHeight = None, windowTitle = None, scene = None, eventManager = None, openGLversion = 4): """Constructor SDL2Window for basic SDL2 parameters :param windowWidth: [description], defaults to None :type windowWidth: [type], optional :param windowHeight: [description], defaults to None :type windowHeight: [type], optional :param windowTitle: [description], defaults to None :type windowTitle: [type], optional """ super().__init__() self._gWindow = None self._gContext = None self._gVersionLabel = "None" self.openGLversion = openGLversion if windowWidth is None: self._windowWidth = 1024 else: self._windowWidth = windowWidth if windowHeight is None: self._windowHeight = 768 else: self._windowHeight = windowHeight if windowTitle is None: self._windowTitle = "SDL2Window" else: self._windowTitle = windowTitle if eventManager is not None and scene is None: # in case we are testing without a Scene and just an EventManager self.eventManager = eventManager if scene is not None: # set the reference of parent RenderWindow to Scene # get the reference to EventManager from Scene.ECSSManager self._scene = scene self.eventManager = scene.world.eventManager #OpenGL state variables self._wireframeMode = False self._colorEditor = 0.0, 0.0, 0.0 self._myCamera = np.identity(4)
@property def gWindow(self): return self._gWindow @property def gContext(self): return self._gContext def init(self): """ Initialise an SDL2 RenderWindow, not directly but via the SDL2Decorator """ print(f'{self.getClassName()}: init()') #SDL_Init for the window initialization sdl_not_initialised = sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO | sdl2.SDL_INIT_TIMER) if sdl_not_initialised !=0: print("SDL2 could not be initialised! SDL Error: ", sdl2.SDL_GetError()) exit(1) #setting OpenGL attributes for the GL state sdl2.SDL_GL_SetAttribute(sdl2.SDL_GL_CONTEXT_FLAGS, sdl2.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG ) sdl2.SDL_GL_SetAttribute(sdl2.SDL_GL_CONTEXT_PROFILE_MASK, sdl2.SDL_GL_CONTEXT_PROFILE_CORE ) sdl2.SDL_GL_SetAttribute(sdl2.SDL_GL_DOUBLEBUFFER, 1) sdl2.SDL_GL_SetAttribute(sdl2.SDL_GL_ACCELERATED_VISUAL, 1) sdl2.SDL_GL_SetAttribute(sdl2.SDL_GL_DEPTH_SIZE, 24) sdl2.SDL_GL_SetAttribute(sdl2.SDL_GL_STENCIL_SIZE, 8) sdl2.SDL_GL_SetAttribute(sdl2.SDL_GL_MULTISAMPLEBUFFERS, 1) if self.openGLversion == 3: print("=" * 24) print("Using OpenGL version 3.2") print("="*24) sdl2.SDL_GL_SetAttribute(sdl2.SDL_GL_CONTEXT_MAJOR_VERSION, 3) sdl2.SDL_GL_SetAttribute(sdl2.SDL_GL_CONTEXT_MINOR_VERSION, 2) else: print("="*24) print("Using OpenGL version 4.1") print("="*24) sdl2.SDL_GL_SetAttribute(sdl2.SDL_GL_CONTEXT_MAJOR_VERSION, 4) sdl2.SDL_GL_SetAttribute(sdl2.SDL_GL_CONTEXT_MINOR_VERSION, 1) # SDL_GL_MULTISAMPLESAMPLES does not work on VMs and some Linux systems, # therefore we depracate it for now # if platform == "linux" or platform == "linux2": # pass # else: # sdl2.SDL_GL_SetAttribute(sdl2.SDL_GL_MULTISAMPLESAMPLES, 16) sdl2.SDL_SetHint(sdl2.SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, b"1") sdl2.SDL_SetHint(sdl2.SDL_HINT_VIDEO_HIGHDPI_DISABLED, b"1") #creating the SDL2 window self._gWindow = sdl2.SDL_CreateWindow(self._windowTitle.encode(), sdl2.SDL_WINDOWPOS_CENTERED, sdl2.SDL_WINDOWPOS_CENTERED, self._windowWidth, self._windowHeight, # sdl2.SDL_WINDOW_ALLOW_HIGHDPI) sdl2.SDL_WINDOW_OPENGL | sdl2.SDL_WINDOW_RESIZABLE | sdl2.SDL_WINDOW_SHOWN ) if self._gWindow is None: print("Window could not be created! SDL Error: ", sdl2.SDL_GetError()) exit(1) #create the OpenGL context for rendering into the SDL2Window that was constructed just before self._gContext = sdl2.SDL_GL_CreateContext(self._gWindow) if self._gContext is None: print("OpenGL Context could not be created! SDL Error: ", sdl2.SDL_GetError()) exit(1) sdl2.SDL_GL_MakeCurrent(self._gWindow, self._gContext) if sdl2.SDL_GL_SetSwapInterval(1) < 0: print("Warning: Unable to set VSync! SDL Error: ", sdl2.SDL_GetError()) # exit(1) #obtain the GL versioning system info self._gVersionLabel = f'OpenGL {gl.glGetString(gl.GL_VERSION).decode()} GLSL {gl.glGetString(gl.GL_SHADING_LANGUAGE_VERSION).decode()} Renderer {gl.glGetString(gl.GL_RENDERER).decode()}' print(self._gVersionLabel) def init_post(self): """ Post init method for SDL2 this should be ctypiically alled AFTER all other GL contexts have been created """ pass def display(self): """ Main display window method to be called standalone or from within a concrete Decorator """ # GPTODO make background clear color as parameter at class level gl.glClearColor(*self._colorEditor, 1.0) gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) gl.glDisable(gl.GL_CULL_FACE) gl.glEnable(gl.GL_DEPTH_TEST) gl.glDepthFunc(gl.GL_LESS) # gl.glDepthFunc(gl.GL_LEQUAL); # gl.glDepthMask(gl.GL_FALSE); # gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL) # setup some extra GL state flags if self._wireframeMode: gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE) #print(f"SDL2Window:display() set wireframemode: {self._wireframeMode}") else: gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL) # print(f"SDL2Window:display() set wireframemode: {self._wireframeMode}") # print(f'{self.getClassName()}: display()') def display_post(self): """ To be called at the end of each drawn frame to swap double buffers """ sdl2.SDL_GL_SwapWindow(self._gWindow) # print(f'{self.getClassName()}: display_post()') def shutdown(self): """ Shutdown and cleanup SDL2 operations """ print(f'{self.getClassName()}: shutdown()') if (self._gContext and self._gWindow is not None): sdl2.SDL_GL_DeleteContext(self._gContext) sdl2.SDL_DestroyWindow(self._gWindow) sdl2.SDL_Quit() def event_input_process(self, running=True): """ process SDL2 basic events and input """ events = sdl2.ext.get_events() for event in events: if event.type == sdl2.SDL_KEYDOWN: if event.key.keysym.sym == sdl2.SDLK_ESCAPE: running = False if event.type == sdl2.SDL_QUIT: running = False if event.type == sdl2.SDL_WINDOWEVENT: window = self.gWindow if event.window.event == sdl2.SDL_WINDOWEVENT_RESIZED: print("Window Resized to ", event.window.data1, " X " , event.window.data2) # new width and height: event.window.data1 and event.window.data2 window._windowWidth = event.window.data1 window._windowHeight = event.window.data2 gl.glViewport(0, 0, event.window.data1, event.window.data2) return running def accept(self, system: Elements.pyECSS.System, event = None): system.apply2SDLWindow(self, event)
[docs]class RenderDecorator(RenderWindow): """ Main Decorator class that wraps a RenderWindow so that all other Decorator classes can dynamically be adding layered functionality on top of the wrapee (RenderWindow) e.g. ImGUI widgets etc. :param RenderWindow: [description] :type RenderWindow: [type] """
[docs] def __init__(self, wrapee: RenderWindow): super().__init__() self._wrapeeWindow = wrapee self._eye = (2.5, 2.5, 2.5) self._target = (0.0, 0.0, 0.0) self._up = (0.0, 1.0, 0.0) # TRS Variables self.translation = {}; self.translation["x"] = 0; self.translation["y"] = 0; self.translation["z"] = 0; self.rotation = {}; self.rotation["x"] = 0; self.rotation["y"] = 0; self.rotation["z"] = 0; self.scale = {}; self.scale["x"] = 0; self.scale["y"] = 0; self.scale["z"] = 0; #this is not used anywhere self.color = [255, 50, 50]; self.lctrl = False self.traverseCamera()
@property def wrapeeWindow(self): return self._wrapeeWindow def init(self): """ [summary] """ self._wrapeeWindow.init() print(f'RenderDecorator: init()') def display(self): """ Main decorator display method """ self._wrapeeWindow.display() def shutdown(self): """ [summary] """ self._wrapeeWindow.shutdown() print(f'RenderDecorator: shutdown()') def traverseCamera(self): self.cam = None found = False if self.wrapeeWindow.scene is not None: rootComp = self.wrapeeWindow.scene.world.root if rootComp._children is not None: Iterator = iter(rootComp._children) done_traversing = False while not found and not done_traversing: try: comp = next(Iterator) except StopIteration: done_traversing = True else: if "camera" in comp.name.lower(): # just put the "Camera" string in the Entity that holds the camera self.cam = comp found = True def createViewMatrix(self, eye, lookAt, upVector): self._eye = tuple(eye) self._target = tuple(lookAt) #self._up = tuple(upVector) #directionVector = util.normalise(lookAt - eye) #rightVector = util.normalise(np.cross(directionVector, upVector)) #upVector = util.normalise(np.cross(rightVector, directionVector)) self._updateCamera.value = util.lookat(eye, lookAt, upVector) def updateCamera(self, moveX, moveY, moveZ, rotateX, rotateY): if self.cam != None: #for examples 7-11 and pyJANVRED implementations cameraspeed = 5 #scaleMat = util.scale(self.scale["x"], self.scale["y"], self.scale["z"]) #combinedMat = scaleMat if rotateX:# or rotateY: #rotMatX = util.rotate((1, 0, 0), -self.rotation["y"] * cameraspeed) rotMatY = util.rotate((0, 1, 0), self.rotation["x"] * cameraspeed) #rotMatZ = util.rotate((0, 0, 1), self.rotation["z"] * cameraspeed) #combinedMat = rotMatX @ rotMatY @ rotMatZ @ combinedMat #combinedMat = rotMatY #@ combinedMat self.cam.trans1.trs = rotMatY @ self.cam.trans1.trs elif rotateY: rotMatX = util.rotate((1, 0, 0), -self.rotation["y"] * cameraspeed) #combinedMat = rotMatX #@ combinedMat self.cam.trans1.trs = self.cam.trans1.trs @ rotMatX if moveX or moveY or moveZ: transMat = util.translate(self.translation["x"], self.translation["y"], -self.translation["z"]) #combinedMat = transMat #@ combinedMat self.cam.trans1.trs = self.cam.trans1.trs @ transMat else: #for examples 4-5-6-8-9-10 implementations cameraspeed = 0.2 teye = np.array(self._eye) ttarget = np.array(self._target) tup = np.array(self._up) forwardDir = util.normalise(ttarget - teye) rightDir = util.normalise(np.cross(forwardDir, tup)) if rotateX: rotMatY = util.rotate(tup, self.rotation["x"] * cameraspeed*15) transMatY = util.translate(ttarget) @ rotMatY @ util.translate(-ttarget) teye = transMatY @ np.append(teye, [1]) teye = teye[:-1] / teye[-1] elif rotateY: rotMatX = util.rotate(rightDir, -self.rotation["y"] * cameraspeed*15) transMatX = util.translate(ttarget) @ rotMatX @ util.translate(-ttarget) teye = transMatX @ np.append(teye, [1]) teye = teye[:-1] / teye[-1] elif moveX or moveY: panX = -cameraspeed * self.translation["x"] * rightDir panY = -self.translation["y"] * cameraspeed * tup teye += panX + panY ttarget += panX + panY elif moveZ: zoom = np.sign(self.translation["z"]) * cameraspeed * forwardDir teye += zoom ttarget += zoom self.createViewMatrix(teye, ttarget, tup) if self._wrapeeWindow.eventManager is not None: self.wrapeeWindow.eventManager.notify(self, self._updateCamera) def on_mouse_motion(self, event, x, y, dx, dy): """Called when the mouse is moved. event: sdl2.events.SDL_Event, x: horiz coord relative to window, y: vert coord relative to window, dx: relative horizontal motion, dy: relative vertical motion """ pass def on_mouse_press(self, event, x, y, button, dclick): """Called when mouse buttons are pressed. event: sdl2.events.SDL_Event, x: horiz coord relative to window, y: vert coord relative to window, dx: relative horizontal motion, dy: relative vertical motion button: RIGHT - MIDDLE - LEFT dclick: True - False if button was double click """ pass def resetAll(self): self.translation["x"] = 0.0 self.translation["y"] = 0.0 self.translation["z"] = 0.0 self.rotation["x"] = 0.0 self.rotation["y"] = 0.0 self.rotation["z"] = 0.0 self.scale["x"]= 1.0 self.scale["y"]= 1.0 self.scale["z"]= 1.0 def cameraHandling(self, x, y, height, width): keystatus = sdl2.SDL_GetKeyboardState(None) self.resetAll() if keystatus[sdl2.SDL_SCANCODE_LSHIFT]: if abs(x) > abs(y): self.translation["x"] = x/width*60 #np.sign(event.wheel.x) self.updateCamera(True, False, False, False, False) else: self.translation["y"] = y/height*60 #np.sign(event.wheel.y) self.updateCamera(False, True, False, False, False) elif keystatus[sdl2.SDL_SCANCODE_LCTRL] or self.lctrl: self.translation["z"] = y/height*60 #-np.sign(event.wheel.y) self.updateCamera(False, False, True, False, False) else: if abs(x) > abs(y): self.rotation["x"] = np.sign(x) #event.wheel.x/height*180 self.updateCamera(False, False,False, True, False) else: self.rotation["y"] = np.sign(y) #event.wheel.y/width*180 self.updateCamera(False, False,False, False, True) # def event_input_process(self): # """ # extra decorator method to handle input events # :param running: [description], defaults to True # :type running: bool, optional # """ # return self._wrapeeWindow.event_input_process() def event_input_process(self): """ process SDL2 basic events and input """ running = True events = sdl2.ext.get_events() width = self.wrapeeWindow._windowWidth height = self.wrapeeWindow._windowHeight ### set up a hot key to easily switch between common keys like shift,ctrl etc ### default at left alt alt_Key = sdl2.KMOD_ALT leftShift_Key = sdl2.KMOD_LSHIFT rightShift_Key = sdl2.KMOD_RSHIFT ctrl_Key = sdl2.KMOD_CTRL shortcut_HotKey = alt_Key for event in events: if event.type == sdl2.SDL_MOUSEWHEEL: x = event.wheel.x y = event.wheel.y self.cameraHandling(x,y,height,width) # on_mouse_press elif event.type == sdl2.SDL_MOUSEMOTION: buttons = event.motion.state if buttons & sdl2.SDL_BUTTON_RMASK: x = -event.motion.xrel y = event.motion.yrel self.cameraHandling(x, y, height, width) #keyboard events elif event.type == sdl2.SDL_KEYDOWN: ################## toggle the wireframe using the alt+F buttons ############################# if (event.key.keysym.sym == sdl2.SDLK_f and (sdl2.SDL_GetModState() & shortcut_HotKey)): self.toggle_Wireframe() ########## shortcuts for selected node from the tree ########### if hasattr(self._wrapeeWindow._scene, "_gContext") and self._wrapeeWindow._scene._gContext.__class__.__name__ == "ImGUIecssDecorator" and self.selected: # we must first check if the ImGUIecssDecorator is active otherwise we will get an error on click ################# - translate on x axis when node is selected using W+alt ########################### if(event.key.keysym.sym == sdl2.SDLK_w and (sdl2.SDL_GetModState() & shortcut_HotKey)): self.translation["x"] -= 0.1 ################# + translate on x axis when node is selected using W ########################### elif(event.key.keysym.sym == sdl2.SDLK_w): self.translation["x"] += 0.1 # ################# - translate on y axis when node is selected using E+alt ########################### if(event.key.keysym.sym == sdl2.SDLK_e and (sdl2.SDL_GetModState() & shortcut_HotKey)): self.translation["y"] -= 0.1 ################# + translate on y axis when node is selected using E ########################### elif(event.key.keysym.sym == sdl2.SDLK_e): self.translation["y"] += 0.1 # ################# - translate on z axis when node is selected using R+alt ########################### if(event.key.keysym.sym == sdl2.SDLK_r and (sdl2.SDL_GetModState() & shortcut_HotKey)): self.translation["z"] -= 0.1 # ################# + translate on z axis when node is selected using R ########################### elif(event.key.keysym.sym == sdl2.SDLK_r): self.translation["z"] += 0.1 # ################# - rotate on x axis when node is selected using T+alt ########################### if(event.key.keysym.sym == sdl2.SDLK_t and (sdl2.SDL_GetModState() & shortcut_HotKey)): self.rotation["x"] -= 0.1 # ################# + rotate on x axis when node is selected using T ########################### elif(event.key.keysym.sym == sdl2.SDLK_t): self.rotation["x"] += 0.1 # ################# - rotate on y axis when node is selected using Y+alt ########################### if(event.key.keysym.sym == sdl2.SDLK_y and (sdl2.SDL_GetModState() & shortcut_HotKey)): self.rotation["y"] -= 0.1 # ################# + rotate on y axis when node is selected using Y ########################### elif(event.key.keysym.sym == sdl2.SDLK_y): self.rotation["y"] += 0.1 # ################# - rotate on z axis when node is selected using U+alt ########################### if(event.key.keysym.sym == sdl2.SDLK_u and (sdl2.SDL_GetModState() & shortcut_HotKey)): self.rotation["z"] -= 0.1 # ################# + rotate on z axis when node is selected using U ########################### elif(event.key.keysym.sym == sdl2.SDLK_u): self.rotation["z"] += 0.1 ################# scale down on x axis when node is selected using I+alt ########################### if(event.key.keysym.sym == sdl2.SDLK_i and (sdl2.SDL_GetModState() & shortcut_HotKey)): self.scale["x"] -= 0.1 ################# scale up on x axis when node is selected using I ########################### elif(event.key.keysym.sym == sdl2.SDLK_i ): self.scale["x"] += 0.1 ################# scale down on y axis when node is selected using O+alt ########################### if(event.key.keysym.sym == sdl2.SDLK_o and (sdl2.SDL_GetModState() & shortcut_HotKey)): self.scale["y"] -= 0.1 ################# scale up on y axis when node is selected using O ########################### elif(event.key.keysym.sym == sdl2.SDLK_o ): self.scale["y"] += 0.1 ################# scale down on z axis when node is selected using P+alt ########################### if(event.key.keysym.sym == sdl2.SDLK_p and (sdl2.SDL_GetModState() & shortcut_HotKey)): self.scale["z"] -= 0.1 ################# scale up on z axis when node is selected using P ########################### elif(event.key.keysym.sym == sdl2.SDLK_p ): self.scale["z"] += 0.1 if event.key.keysym.sym == sdl2.SDLK_ESCAPE: running = False elif event.type == sdl2.SDL_KEYUP and event.key.keysym.sym == sdl2.SDLK_LCTRL: self.lctrl = False elif event.type == sdl2.SDL_QUIT: running = False elif event.type == sdl2.SDL_WINDOWEVENT: window = self.wrapeeWindow if event.window.event == sdl2.SDL_WINDOWEVENT_RESIZED: print("Window Resized to ", event.window.data1, " X " , event.window.data2) window._windowWidth = event.window.data1 window._windowHeight = event.window.data2 # new width and height: event.window.data1 and event.window.data2 gl.glViewport(0, 0, event.window.data1, event.window.data2) #imgui event self._imguiRenderer.process_event(event) #imgui input self._imguiRenderer.process_inputs() return running #self._wrapeeWindow.event_input_process() & running def display_post(self): """ Post diplay method after all other display calls have been issued """ self._wrapeeWindow.display_post() def init_post(self): """ Post init method this should be ctypiically alled AFTER all other GL contexts have been created, e.g. ImGUI context """ self._wrapeeWindow.init_post() def accept(self, system: Elements.pyECSS.System, event = None): pass
[docs]class RenderGLStateSystem(System): """ System that operates on a RenderDecorator (ImGUIDecorator) and affect GL State """
[docs] def __init__(self, name=None, type=None, id=None): super().__init__(name, type, id)
def update(self): """ method to be subclassed for behavioral or logic computation """ pass def apply2ImGUIDecorator(self, imGUIDecorator, event = None): """ method for behavioral or logic computation when visits Components. In this case update GL State from ImGUIDecorator """ pass def apply2SDLWindow(self, sdlWindow, event=None): """method for behavioral or logic computation when visits Components. In this case update GL State from SDLWindow :param sdlWindow: [description] :type sdlWindow: [type] :param event: [description], defaults to None :type event: [type], optional """ if event.name == "OnUpdateWireframe": # print(f"RenderGLStateSystem():apply2SDLWindow() actuator system for: {event}") sdlWindow._wireframeMode = event.value if event.name == "OnUpdateCamera": # print(f"OnUpdateCamera: RenderGLStateSystem():apply2SDLWindow() actuator system for: {event}") sdlWindow._myCamera = event.value
if __name__ == "__main__": # The client code. gWindow = SDL2Window(openGLversion=3) # uses openGL version 3.2 instead of the default 4.1 gWindow.init() gWindow.init_post() running = True # MAIN RENDERING LOOP while running: gWindow.display() running = gWindow.event_input_process(running) gWindow.display_post() gWindow.shutdown()