Source code for psychopy.visual.nnlvs

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Window class for the NordicNeuralLab's VisualSystemHD(tm) fMRI display


# Part of the PsychoPy library
# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019 Open Science Tools Ltd.
# Distributed under the terms of the GNU General Public License (GPL).

import ctypes
import numpy as np
import as GL
from psychopy.visual import window
from psychopy import logging
import as mt
import as vt
import as gt

    from PIL import Image
except ImportError:
    import Image

reportNDroppedFrames = 5

# ------------------------------------------------------------------------------
# Configurations for the model of VSHD

vshdModels = {
    'vshd': {
        # store the configuration for FOV and viewing distance by diopters
        'configByDiopters': {
            -9: {'vfov': 29.70, 'dist': 0.111},
            -8: {'vfov': 29.75, 'dist': 0.125},
            -7: {'vfov': 29.8, 'dist': 0.143},
            -6: {'vfov': 29.86, 'dist': 0.167},
            -5: {'vfov': 29.92, 'dist': 0.20},
            -4: {'vfov': 29.98, 'dist': 0.25},
            -3: {'vfov': 30.04, 'dist': 0.333},
            -2: {'vfov': 30.08, 'dist': 0.5},
            -1: {'vfov': 30.14, 'dist': 1.0},
            0:  {'vfov': 30.18, 'dist': 1.0},  # TODO: handle this case
            1:  {'vfov': 30.24, 'dist': -1.0},  # image gets inverted
            2:  {'vfov': 30.3, 'dist': -0.5},
            3:  {'vfov': 30.36, 'dist': -0.333},
            4:  {'vfov': 30.42, 'dist': -0.25},
            5:  {'vfov': 30.46, 'dist': -0.2},
        'scrHeightM': 9.6 * 1200. / 1e-6,  # screen height in meters
        'scrWidthM': 9.6 * 1920. / 1e-6,  # screen width in meters
        'distCoef': -0.02,  # distortion coef. depends on screen size
        'resolution': (1920, 1200)  # resolution for the display per eye

# ------------------------------------------------------------------------------
# Window subclass class for VSHD hardware

[docs]class VisualSystemHD(window.Window): """Class provides support for NordicNeuralLab's VisualSystemHD(tm) fMRI display hardware. Use this class in-place of the `Window` class for use with the VSHD hardware. Ensure that the VSHD headset display output is configured in extended desktop mode (eg. nVidia Surround). Extended desktops are only supported on Windows and Linux systems. The VSHD is capable of both 2D and stereoscopic 3D rendering. You can select which eye to draw to by calling `setBuffer`, much like how stereoscopic rendering is implemented in the base `Window` class. Notes ----- * This class handles drawing differently than the default window class, as a result, stimuli `autoDraw` is not supported. * Edges of the warped image may appear jagged. To correct this, create a window using `multiSample=True` and `numSamples > 1` to smooth out these artifacts. Examples -------- Here is a basic example of 2D rendering using the VisualSystemHD(tm). This is the binocular version of the dynamic '' demo:: from psychopy import visual, core, event # Create a visual window win = visual.VisualSystemHD(fullscr=True, screen=1) # Initialize some stimuli, note contrast, opacity, ori grating1 = visual.GratingStim(win, mask="circle", color='white', contrast=0.5, size=(1.0, 1.0), sf=(4, 0), ori = 45, autoLog=False) grating2 = visual.GratingStim(win, mask="circle", color='white', opacity=0.5, size=(1.0, 1.0), sf=(4, 0), ori = -45, autoLog=False, pos=(0.1, 0.1)) trialClock = core.Clock() t = 0 while not event.getKeys() and t < 20: t = trialClock.getTime() for eye in ('left', 'right'): win.setBuffer(eye) # change the buffer grating1.phase = 1 * t # drift at 1Hz grating1.draw() # redraw it grating2.phase = 2 * t # drift at 2Hz grating2.draw() # redraw it win.flip() win.close() core.quit() As you can see above, there are few changes needed to convert an existing 2D experiment to run on the VSHD. For 3D rendering with perspective, you need set `eyeOffset` and apply the projection by calling `setPerspectiveView`. (other projection modes are not implemented or supported right now):: from psychopy import visual, core, event # Create a visual window win = visual.VisualSystemHD(fullscr=True, screen=1, multiSample=True, nSamples=8) # text to display instr = visual.TextStim(win, text="Any key to quit", pos=(0, -.7)) # create scene light at the pivot point win.lights = [ visual.LightSource(win, pos=(0.4, 4.0, -2.0), lightType='point', diffuseColor=(0, 0, 0), specularColor=(1, 1, 1)) ] win.ambientLight = (0.2, 0.2, 0.2) # Initialize some stimuli, note contrast, opacity, ori ball = visual.SphereStim(win, radius=0.1, pos=(0, 0, -2), color='green', useShaders=False) iod = 6.2 # interocular separation in CM win.setEyeOffset(-iod / 2.0, 'left') win.setEyeOffset(iod / 2.0, 'right') trialClock = core.Clock() t = 0 while not event.getKeys() and t < 20: t = trialClock.getTime() for eye in ('left', 'right'): win.setBuffer(eye) # change the buffer # setup drawing with perspective win.setPerspectiveView() win.useLights = True # switch on lights ball.draw() # draw the ball # shut the lights, needed to prevent light color from affecting # 2D stim win.useLights = False # reset transform to draw text correctly win.resetEyeTransform() instr.draw() win.flip() win.close() core.quit() """ def __init__(self, monoscopic=False, diopters=(-1, -1), lensCorrection=True, distCoef=None, directDraw=False, model='vshd', *args, **kwargs): """ Parameters ---------- monoscopic : bool Use monoscopic rendering. If `True`, the same image will be drawn to both eye buffers. You will not need to call `setBuffer`. It is not possible to set monoscopic mode after the window is created. It is recommended that you use monoscopic mode if you intend to display only 2D stimuli about the center of the display as it uses a less memory intensive rendering pipeline. diopters : tuple or list Initial diopter values for the left and right eye. Default is `(-1, -1)`, values must be integers. lensCorrection : bool Apply lens correction (barrel distortion) to the output. The amount of distortion applied can be specified using `distCoef`. If `False`, no distortion will be applied to the output and the entire display will be used. Not applying correction will result in pincushion distortion which produces a non-rectilinear output. distCoef : float Distortion coefficient for barrel distortion. If `None`, the recommended value will be used for the model of display. You can adjust the value to fine-tune the barrel distortion. directDraw : bool Direct drawing mode. Stimuli are drawn directly to the back buffer instead of creating separate buffer. This saves video memory but does not permit barrel distortion or monoscopic rendering. If `False`, drawing is done with two FBOs containing each eye's image. hwModel : str Model of the VisualSystemHD in use. Used to set viewing parameters accordingly. Default is 'vshd'. Cannot be changed after starting the application. """ # warn if given `useFBO` if kwargs.get('useFBO', False): logging.warning( "Creating a window with `useFBO` is not recommended.") # call up a new window object super(VisualSystemHD, self).__init__(*args, **kwargs) # direct draw mode self._directDraw = directDraw # is monoscopic mode enabled? self._monoscopic = monoscopic and not self._directDraw self._lensCorrection = lensCorrection and not self._directDraw # hardware information for a given model of the display, used for # configuration self._hwModel = model self._hwDesc = vshdModels[self._hwModel] # distortion coefficient self._distCoef = \ self._hwDesc['distCoef'] if distCoef is None else float(distCoef) # diopter settings for each eye, needed to compute actual FOV self._diopters = {'left': diopters[0], 'right': diopters[1]} # eye offsets, this will be standard when multi-buffer rendering gets # implemented in the main window class self._eyeOffsets = {'left': -3.1, 'right': 3.1} # look-up table of FOV values for each diopter setting self._fovLUT = self._hwDesc['configByDiopters'] # get the dimensions of the buffer for each eye bufferWidth, bufferHieght = self.frameBufferSize bufferWidth = int(bufferWidth / 2.0) # viewports for each buffer self._bufferViewports = dict() self._bufferViewports['left'] = (0, 0, bufferWidth, bufferHieght) self._bufferViewports['right'] = \ (bufferWidth if self._directDraw else 0, 0, bufferWidth, bufferHieght) self._bufferViewports['back'] = \ (0, 0, self.frameBufferSize[0], self.frameBufferSize[1]) # create render targets for each eye buffer self.buffer = None self._eyeBuffers = dict() # VAOs for distortion mesh self._warpVAOs = dict() # extents of the barrel distortion needed to compute FOV after # distortion self._distExtents = { 'left': np.array([[-1, 0], [1, 0], [0, 1], [0, -1]]), 'right': np.array([[-1, 0], [1, 0], [0, 1], [0, -1]]) } # if we are using an FBO, keep a reference to its handles if self.useFBO: self._eyeBuffers['back'] = { 'frameBuffer': self.frameBuffer, 'frameTexture': self.frameTexture, 'frameStencil': self._stencilTexture} self._setupEyeBuffers() # setup additional framebuffers self._setupLensCorrection() # setup lens correction meshes self.setBuffer('left') # set to the back buffer on start @property def monoscopic(self): """`True` if using monoscopic mode.""" return self._monoscopic @property def lensCorrection(self): """`True` if using lens correction.""" return self._lensCorrection @lensCorrection.setter def lensCorrection(self, value): self._lensCorrection = value @property def distCoef(self): """Distortion coefficient (`float`).""" return self._distCoef @distCoef.setter def distCoef(self, value): if isinstance(value, (float, int,)): self._distCoef = float(value) elif value is None: self._distCoef = self._hwDesc['distCoef'] else: raise ValueError('Invalid value for `distCoef`.') self._setupLensCorrection() # deletes old VAOs @property def diopters(self): """Diopters value of the current eye buffer.""" return self._diopters[self.buffer] @diopters.setter def diopters(self, value): self.setDiopters(value)
[docs] def setDiopters(self, diopters, eye=None): """Set the diopters for a given eye. Parameters ---------- diopters : int Set diopters for a given eye, ranging between -7 and +5. eye : str or None Eye to set, either 'left' or 'right'. If `None`, the currently set buffer will be used. """ eye = self.buffer if eye is None else eye # check if diopters value in config if diopters not in self._hwDesc['configByDiopters'].keys(): raise ValueError("Diopter setting invalid for display model.") try: self._diopters[eye] = int(diopters) except KeyError: raise ValueError( "Invalid `eye` specified, must be 'left' or 'right'.")
@property def eyeOffset(self): """Eye offset for the current buffer in centimeters used for stereoscopic rendering. This works differently than the main window class as it sets the offset for the current buffer. The offset is saved and automatically restored when the buffer is selected. """ return self._eyeOffsets[self.buffer] * 100. # to centimeters @eyeOffset.setter def eyeOffset(self, value): self._eyeOffsets[self.buffer] = float(value) / 100. # to meters
[docs] def setEyeOffset(self, dist, eye=None): """Set the eye offset in centimeters. When set, successive rendering operations will use the new offset. Parameters ---------- dist : float or int Lateral offset in centimeters from the nose, usually half the interocular separation. The distance is signed. eye : str or None Eye offset to set. Can either be 'left', 'right' or `None`. If `None`, the offset of the current buffer is used. """ eye = self.buffer if eye is None else eye try: self._eyeOffsets[eye] = float(dist) / 100. # to meters except KeyError: raise ValueError( "Invalid `eye` specified, must be 'left' or 'right'.")
# def _getFocalLength(self, eye=None): # """Get the focal length for a given `eye` in meters. # # Parameters # ---------- # eye : str or None # Eye to use, either 'left' or 'right'. If `None`, the currently # set buffer will be used. # # """ # try: # focalLength = 1. / self._diopters[eye] # except KeyError: # raise ValueError( # "Invalid value for `eye`, must be 'left' or 'right'.") # except ZeroDivisionError: # raise ValueError("Value for diopters cannot be zero.") # # return focalLength # # def _getMagFactor(self, eye=None): # """Get the magnification factor of the lens for a given eye. Used to # in part to compute the actual size of the object. # # Parameters # ---------- # eye : str or None # Eye to use, either 'left' or 'right'. If `None`, the currently # set buffer will be used. # # """ # eye = self.buffer if eye is None else eye # return (self._diopters[eye] / 4.) + 1. # # def _getScreenFOV(self, eye, direction='horizontal', degrees=True): # """Compute the FOV of the the display.""" # if direction not in ('horizontal', 'vertical'): # raise ValueError("Invalid `direction` specified, must be " # "'horizontal' or 'vertical'.") # # # todo: figure this out # # def _getPredictedFOV(self, size, eye=None): # """Get the predicted vertical FOV of the display for a given eye. # # Parameters # ---------- # size : float # Size of the object on screen in meters. # eye : str or None # Eye to use, either 'left' or 'right'. If `None`, the currently # set buffer will be used. # # """ # return np.degrees(2. * np.arctan(size / (2. * self._getFocalLength(eye)))) # # def _getActualFOV(self, size, eye=None): # """Get the actual FOV of an object of `size` for a given eye.""" # if eye not in ('left', 'right'): # raise ValueError( # "Invalid value for `eye`, must be 'left' or 'right'.") # # # predFOV = self._getPredictedFOV(size, eye) # actualFOV = 0.0098 * size ** 3 - 0.0576 * size ** 2 + 2.6728 * size - 0.0942 # # return actualFOV # # def getDistortion(self, size, eye=None): # """Get the optical distortion amount (percent) for a stimulus of given # `size` in degrees positioned at the center of the display.""" # predFOV = self._getPredictedFOV(size, eye) # actualFOV = self._getActualFOV(size, eye) # # return ((actualFOV - predFOV) * predFOV) * 100.
[docs] def _getWarpExtents(self, eye): """Get the horizontal and vertical extents of the barrel distortion in normalized device coordinates. This is used to determine the FOV along each axis after barrel distortion. Parameters ---------- eye : str Eye to compute the extents for. Returns ------- ndarray 2d array of coordinates [+X, -X, +Y, -Y] of the extents of the barrel distortion. """ coords = np.array([ [-1.0, 0.0], [ 1.0, 0.0], [ 0.0, 1.0], [ 0.0, -1.0]]) try: bufferW, bufferH = self._bufferViewports[eye][2:] except KeyError: raise ValueError("Invalid eye buffer specified.") warpedCoords = mt.lensCorrectionSpherical( coords, self._distCoef, bufferW / bufferH) return warpedCoords
[docs] def _setupLensCorrection(self): """Setup the VAOs needed for lens correction. """ # don't create warp mesh if direct draw enabled if self._directDraw: return # clean up previous distortion data if self._warpVAOs: for vao in self._warpVAOs.values(): gt.deleteVAO(vao) self._warpVAOs = {} for eye in ('left', 'right'): # setup vertex arrays for the output quad bufferW, bufferH = self._bufferViewports[eye][2:] aspect = bufferW / bufferH vertices, texCoord, normals, faces = gt.createMeshGrid( (2.0, 2.0), subdiv=256, tessMode='radial') # recompute vertex positions vertices[:, :2] = mt.lensCorrectionSpherical( vertices[:, :2], coefK=self.distCoef, aspect=aspect) # create the VAO for the eye buffer vertexVBO = gt.createVBO(vertices, usage=GL.GL_DYNAMIC_DRAW) texCoordVBO = gt.createVBO(texCoord, usage=GL.GL_DYNAMIC_DRAW) indexBuffer = gt.createVBO( faces.flatten(), target=GL.GL_ELEMENT_ARRAY_BUFFER, dataType=GL.GL_UNSIGNED_INT) attribBuffers = {GL.GL_VERTEX_ARRAY: vertexVBO, GL.GL_TEXTURE_COORD_ARRAY: texCoordVBO} self._warpVAOs[eye] = gt.createVAO(attribBuffers, indexBuffer=indexBuffer, legacy=True) # get the extents of the warping self._distExtents[eye] = self._getWarpExtents(eye)
[docs] def _setupEyeBuffers(self): """Setup additional buffers for rendering content to each eye. """ if self._directDraw: # don't create additional buffers is direct draw return def createFramebuffer(width, height): """Function for setting up additional buffer""" # create a new framebuffer fboId = GL.GLuint() GL.glGenFramebuffers(1, ctypes.byref(fboId)) GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, fboId) # create a texture to render to, required for warping texId = GL.GLuint() GL.glGenTextures(1, ctypes.byref(texId)) GL.glBindTexture(GL.GL_TEXTURE_2D, texId) GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR) GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR) GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA32F_ARB, int(width), int(height), 0, GL.GL_RGBA, GL.GL_FLOAT, None) GL.glFramebufferTexture2D(GL.GL_FRAMEBUFFER, GL.GL_COLOR_ATTACHMENT0, GL.GL_TEXTURE_2D, texId, 0) # create a render buffer rbId = GL.GLuint() GL.glGenRenderbuffers(1, ctypes.byref(rbId)) GL.glBindRenderbuffer(GL.GL_RENDERBUFFER, rbId) GL.glRenderbufferStorage( GL.GL_RENDERBUFFER, GL.GL_DEPTH24_STENCIL8, int(width), int(height)) GL.glFramebufferRenderbuffer( GL.GL_FRAMEBUFFER, GL.GL_DEPTH_ATTACHMENT, GL.GL_RENDERBUFFER, rbId) GL.glFramebufferRenderbuffer( GL.GL_FRAMEBUFFER, GL.GL_STENCIL_ATTACHMENT, GL.GL_RENDERBUFFER, rbId) GL.glBindRenderbuffer(GL.GL_RENDERBUFFER, 0) # clear the buffer GL.glClear(GL.GL_COLOR_BUFFER_BIT) GL.glClear(GL.GL_STENCIL_BUFFER_BIT) GL.glClear(GL.GL_DEPTH_BUFFER_BIT) GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0) return fboId, texId, rbId if not self._monoscopic: # create eye buffers for eye in ('left', 'right'): # get dimensions of required buffer bufferW, bufferH = self._bufferViewports[eye][2:] fboId, texId, rbId = createFramebuffer(bufferW, bufferH) # add new buffers self._eyeBuffers[eye] = { 'frameBuffer': fboId, 'frameTexture': texId, 'frameStencil': rbId} else: # only the left eye buffer is created bufferW, bufferH = self._bufferViewports['left'][2:] fboId, texId, rbId = createFramebuffer(bufferW, bufferH) self._eyeBuffers['left'] = { 'frameBuffer': fboId, 'frameTexture': texId, 'frameStencil': rbId} self._eyeBuffers['right'] = self._eyeBuffers['left']
[docs] def setBuffer(self, buffer, clear=True): """Set the eye buffer to draw to. Subsequent draw calls will be diverted to the specified eye. Parameters ---------- buffer : str Eye buffer to draw to. Values can either be 'left' or 'right'. clear : bool Clear the buffer prior to drawing. """ # check if the buffer name is valid if buffer not in ('left', 'right', 'back'): raise RuntimeError("Invalid buffer name specified.") # don't change the buffer if same, but allow clearing if buffer != self.buffer: if not self._directDraw: # handle when the back buffer is selected if buffer == 'back': if self.useFBO: GL.glBindFramebuffer( GL.GL_FRAMEBUFFER, self._eyeBuffers[buffer]['frameBuffer']) else: GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0) else: GL.glBindFramebuffer( GL.GL_FRAMEBUFFER, self._eyeBuffers[buffer]['frameBuffer']) self.viewport = self.scissor = self._bufferViewports[buffer] GL.glEnable(GL.GL_SCISSOR_TEST) self.buffer = buffer # set buffer string if clear: self.setColor(self.color) # clear the buffer to the window color GL.glClearDepth(1.0) GL.glDepthMask(GL.GL_TRUE) GL.glClear( GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT | GL.GL_STENCIL_BUFFER_BIT) GL.glDisable(GL.GL_TEXTURE_2D) GL.glEnable(GL.GL_BLEND)
[docs] def setPerspectiveView(self, applyTransform=True, clearDepth=True): """Set the projection and view matrix to render with perspective. Matrices are computed using values specified in the monitor configuration with the scene origin on the screen plane. Calculations assume units are in meters. If `eyeOffset != 0`, the view will be transformed laterally, however the frustum shape will remain the same. Note that the values of :py:attr:`~Window.projectionMatrix` and :py:attr:`~Window.viewMatrix` will be replaced when calling this function. Parameters ---------- applyTransform : bool Apply transformations after computing them in immediate mode. Same as calling :py:attr:`~Window.applyEyeTransform()` afterwards if `False`. clearDepth : bool, optional Clear the depth buffer. """ # Not in full screen mode? Need to compute the dimensions of the display # area to ensure disparities are correct even when in windowed-mode. aspect = self.size[0] / self.size[1] # use these instead of those from the monitor configuration vfov = self._fovLUT[self._diopters[self.buffer]]['vfov'] scrDist = self._fovLUT[self._diopters[self.buffer]]['dist'] frustum = vt.computeFrustumFOV( vfov, aspect, # aspect ratio scrDist, nearClip=self._nearClip, farClip=self._farClip) self._projectionMatrix = \ vt.perspectiveProjectionMatrix(*frustum, dtype=np.float32) # translate away from screen self._viewMatrix = np.identity(4, dtype=np.float32) self._viewMatrix[0, 3] = -self._eyeOffsets[self.buffer] # apply eye offset self._viewMatrix[2, 3] = -scrDist # displace scene away from viewer if applyTransform: self.applyEyeTransform(clearDepth=clearDepth)
[docs] def _blitEyeBuffer(self, eye): """Warp and blit to the appropriate eye buffer. Parameters ---------- eye : str Eye buffer being used. """ GL.glBindTexture(GL.GL_TEXTURE_2D, self._eyeBuffers[eye]['frameTexture']) GL.glEnable(GL.GL_SCISSOR_TEST) # set the viewport and scissor rect for the buffer frameW, frameH = self._bufferViewports[eye][2:] offset = 0 if eye == 'left' else frameW self.viewport = self.scissor = (offset, 0, frameW, frameH) GL.glMatrixMode(GL.GL_PROJECTION) GL.glLoadIdentity() GL.glOrtho(-1, 1, -1, 1, -1, 1) GL.glMatrixMode(GL.GL_MODELVIEW) GL.glLoadIdentity() # anti-aliasing the edges of the polygon GL.glEnable(GL.GL_MULTISAMPLE) # blit the quad covering the side of the display the eye is viewing if self.lensCorrection: gt.drawVAO(self._warpVAOs[eye]) else: self._renderFBO() GL.glDisable(GL.GL_MULTISAMPLE) # reset GL.glDisable(GL.GL_SCISSOR_TEST) self.viewport = self.scissor = \ (0, 0, self.frameBufferSize[0], self.frameBufferSize[1])
[docs] def _startOfFlip(self): """Custom :py:class:`~Rift._startOfFlip` for HMD rendering. This finalizes the HMD texture before diverting drawing operations back to the on-screen window. This allows :py:class:`~Rift.flip` to swap the on-screen and HMD buffers when called. This function always returns `True`. Returns ------- True """ # nop if we are still setting up the window if not hasattr(self, '_eyeBuffers'): return True # direct draw being used, don't do FBO blit if self._directDraw: return True # Switch off multi-sampling # GL.glDisable(GL.GL_MULTISAMPLE) oldColor = self.color self.setColor((-1, -1, -1)) self.setBuffer('back', clear=True) self._prepareFBOrender() # need blit the framebuffer object to the actual back buffer # unbind the framebuffer as the render target GL.glDisable(GL.GL_BLEND) stencilOn = self.stencilTest self.stencilTest = False # before flipping need to copy the renderBuffer to the # frameBuffer GL.glActiveTexture(GL.GL_TEXTURE0) GL.glEnable(GL.GL_TEXTURE_2D) GL.glColor3f(1.0, 1.0, 1.0) # glColor multiplies with texture GL.glColorMask(True, True, True, True) # blit the textures to the back buffer for eye in ('left', 'right'): self._blitEyeBuffer(eye) GL.glEnable(GL.GL_BLEND) self._finishFBOrender() self.stencilTest = stencilOn self.setColor(oldColor) # This always returns True return True
[docs] def _endOfFlip(self, clearBuffer): """Override end of flip with custom color channel masking if required. """ if clearBuffer: GL.glClear(GL.GL_COLOR_BUFFER_BIT) # nop if we are still setting up the window if hasattr(self, '_eyeBuffers'): self.setBuffer('left', clear=clearBuffer)
if __name__ == "__main__": pass

Back to top