def _tidy(self): to_del = [] for i in self.textures_dict: tex = self.textures_dict[i] if tex[1] == 1: opengles.glDeleteTextures(GLsizei(1), byref(tex[0])) to_del.append(i) for i in to_del: del self.textures_dict[i] to_del = [] for i in self.vbufs_dict: vbuf = self.vbufs_dict[i] if vbuf[1] == 1: opengles.glDeleteBuffers(GLsizei(1), byref(vbuf[0])) to_del.append(i) for i in to_del: del self.vbufs_dict[i] to_del = [] for i in self.ebufs_dict: ebuf = self.ebufs_dict[i] if ebuf[1] == 1: opengles.glDeleteBuffers(GLsizei(1), byref(ebuf[0])) to_del.append(i) for i in to_del: del self.ebufs_dict[i] self.tidy_needed = False
def masked_screenshot(x, y, w, h): """returns numpy array from part of screen so it can be used by applications drawing low resolution offscreen textures using scaling. """ img = np.zeros((h, w, 4), dtype=np.uint8) opengles.glReadPixels(GLint(x), GLint(y), GLsizei(w), GLsizei(h), GL_RGBA, GL_UNSIGNED_BYTE, img.ctypes.data_as(ctypes.POINTER(GLubyte))) return img[::-1, :, :3].copy()
def draw(self, shape): """ draw the shape using the clashtest Shader Arguments: *shape* Shape object that will be drawn """ if not self.s_flg: opengles.glEnable(GL_SCISSOR_TEST) opengles.glScissor(GLint(0), GLint(self.y0), GLsizei(self.ix), GLsizei(1)) self.s_flg = True shape.draw(shader=self.shader)
def start_capture(self, clear=True): """ after calling this method all object.draw()s will rendered to this texture and not appear on the display. Large objects will obviously take a while to draw and re-draw """ super(PostProcess, self)._start(clear=clear) from pi3d.Display import Display xx = int(Display.INSTANCE.width / 2.0 * (1.0 - self.scale)) yy = int(Display.INSTANCE.height / 2.0 * (1.0 - self.scale)) ww = int(Display.INSTANCE.width * self.scale) hh = int(Display.INSTANCE.height * self.scale) opengles.glEnable(GL_SCISSOR_TEST) opengles.glScissor(GLint(xx), GLint(yy), GLsizei(ww), GLsizei(hh))
def repaint(self, t): self.move() self.bounce_wall(Display.INSTANCE.width, Display.INSTANCE.height) if t == 0: #TODO this is not good but there needs to be a way to say last ball! opengles.glScissor(GLint(0), GLint(0), GLsizei(int(Display.INSTANCE.width)), GLsizei(int(Display.INSTANCE.height))) #NB the screen coordinates for glScissor have origin in BOTTOM left else: opengles.glScissor( GLint(int(self.or_x + self.unif[0] - self.radius - 5)), GLint(int(self.or_y + self.unif[1] - self.radius - 5)), GLsizei(self.w + 10), GLsizei(self.h + 10)) self.draw()
def __init__(self, name=""): """ new system doesn't use Texture """ from pi3d.Display import Display self.disp = Display.INSTANCE self.ix, self.iy = self.disp.width, self.disp.height color = GLuint() depth = GLuint() self.framebuffer = (GLuint * 1)() pi3d.opengles.glGenFramebuffers(GLsizei(1), self.framebuffer) self.depthbuffer = (GLuint * 1)() pi3d.opengles.glGenRenderbuffers(GLsizei(1), self.depthbuffer) pi3d.opengles.glGenTextures(1, ctypes.byref(color)) pi3d.opengles.glBindTexture(GL_TEXTURE_2D, color) pi3d.opengles.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) pi3d.opengles.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) pi3d.opengles.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) pi3d.opengles.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) pi3d.opengles.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.ix, self.iy, 0, GL_RGBA, GL_UNSIGNED_BYTE, None) pi3d.opengles.glGenerateMipmap(GL_TEXTURE_2D) pi3d.opengles.glBindTexture(GL_TEXTURE_2D, 0) pi3d.opengles.glGenTextures(1, ctypes.byref(depth)) pi3d.opengles.glBindTexture(GL_TEXTURE_2D, depth) pi3d.opengles.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) pi3d.opengles.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) pi3d.opengles.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) pi3d.opengles.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) pi3d.opengles.glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, self.ix, self.iy, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, None) pi3d.opengles.glGenerateMipmap(GL_TEXTURE_2D) pi3d.opengles.glBindTexture(GL_TEXTURE_2D, 0) pi3d.opengles.glEnable(GL_TEXTURE_2D) self.color = TextureShell(color, self.ix, self.iy, False, True) self.depth = TextureShell(depth, self.ix, self.iy, False, True)
def showprogramlog(self, shader): """Prints the compile log for a program""" N = 1024 log = (ctypes.c_char * N)() loglen = GLsizei() opengles.glGetProgramInfoLog( shader, N, ctypes.byref(loglen), log)
def __init__(self, name): """ calls Texture.__init__ but doesn't need to set file name as texture generated from the framebuffer """ super(OffScreenTexture, self).__init__(name) from pi3d.Display import Display self.disp = Display.INSTANCE self.ix, self.iy = self.disp.width, self.disp.height self.image = np.zeros((self.iy, self.ix, 4), dtype=np.uint8) self.blend = False self.mipmap = False self._tex = GLuint() self.framebuffer = (GLuint * 1)() pi3d.opengles.glGenFramebuffers(GLsizei(1), self.framebuffer) self.depthbuffer = (GLuint * 1)() pi3d.opengles.glGenRenderbuffers(GLsizei(1), self.depthbuffer)
def showshaderlog(self, shader, src): """Prints the compile log for a shader""" N = 1024 log = (ctypes.c_char * N)() loglen = GLsizei() opengles.glGetShaderInfoLog(shader, N, ctypes.byref(loglen), log) if len(log.value) > 0: LOGGER.debug('shader(%s) %s, %s', shader, self.shfile, log.value)
def _load_opengl(self): self.vbuf = GLuint() opengles.glGenBuffers(GLsizei(1), ctypes.byref(self.vbuf)) self.ebuf = GLuint() opengles.glGenBuffers(GLsizei(1), ctypes.byref(self.ebuf)) self.disp.vbufs_dict[str(self.vbuf)] = [self.vbuf, 0] self.disp.ebufs_dict[str(self.ebuf)] = [self.ebuf, 0] self._select() opengles.glBufferData(GL_ARRAY_BUFFER, self.array_buffer.nbytes, self.array_buffer.ctypes.data_as(ctypes.POINTER(GLfloat)), GL_STATIC_DRAW) opengles.glBufferData(GL_ELEMENT_ARRAY_BUFFER, self.element_array_buffer.nbytes, self.element_array_buffer.ctypes.data_as(ctypes.POINTER(GLfloat)), GL_STATIC_DRAW) if opengles.glGetError() == GL_OUT_OF_MEMORY: LOGGER.critical('Out of GPU memory')
def start_capture(self, side): """ after calling this method all object.draw()s will rendered to this texture and not appear on the display. *side* Either 0 or 1 to determine stereoscopic view """ self.camera_3d.reset() offs = -self.offs if side == 0 else self.offs self.camera_3d.offset([offs, 0.0, 0.0]) #self.camera_3d.mtrx = np.dot(self.forMtrx, self.camera_3d.mtrx) #self.camera_3d.position(self.position) tex = self.textures[side] tex._start() if self.interlace <= 0: xx = tex.ix / 4.0 # draw the middle only - half width yy = 0 ww = tex.ix / 2.0 hh = tex.iy opengles.glEnable(GL_SCISSOR_TEST) opengles.glScissor(GLint(int(xx)), GLint(int(yy)), GLsizei(int(ww)), GLsizei(int(hh)))
def check(self, grain=50): """ checks the pixels of the texture to see if there is any change from the first pixel sampled; in which case returns True else returns False. Keyword argument: *grain* Number of locations to check over the whole image NB this is no longer used - there are fixed 100 checks across the full width at the mid y position. This self.setp value is set in __init__() """ opengles.glDisable(GL_SCISSOR_TEST) self.s_flg = False img = self.img # alias to make code a bit less bulky! opengles.glReadPixels(GLint(0), GLint(self.y0), GLsizei(self.ix), GLsizei(1), GL_RGBA, GL_UNSIGNED_BYTE, img.ctypes.data_as(ctypes.POINTER(GLubyte))) if (np.any(img[::self.step, 0] != img[0, 0]) or np.any(img[::self.step, 1] != img[0, 1]) or np.any(img[::self.step, 2] != img[0, 2])): return True return False
def delete_buffers(self): pi3d.opengles.glDeleteFramebuffers(GLsizei(1), self.framebuffer) pi3d.opengles.glDeleteRenderbuffers(GLsizei(1), self.depthbuffer)
def create_surface(self, x=0, y=0, w=0, h=0, layer=0): #Set the viewport position and size dst_rect = c_ints((x, y, w, h)) src_rect = c_ints((x, y, w << 16, h << 16)) if PLATFORM == PLATFORM_ANDROID: self.surface = openegl.eglGetCurrentSurface(EGL_DRAW) # Get the width and height of the screen - TODO, this system returns 100x100 time.sleep(0.2) #give it a chance to find out the dimensions w = c_int() h = c_int() openegl.eglQuerySurface(self.display, self.surface, EGL_WIDTH, byref(w)) openegl.eglQuerySurface(self.display, self.surface, EGL_HEIGHT, byref(h)) self.width, self.height = w.value, h.value elif PLATFORM == PLATFORM_PI: self.dispman_display = bcm.vc_dispmanx_display_open( 0) #LCD setting self.dispman_update = bcm.vc_dispmanx_update_start(0) alpha = c_ints((DISPMANX_FLAGS_ALPHA_PREMULT, 0, 0)) self.dispman_element = bcm.vc_dispmanx_element_add( self.dispman_update, self.dispman_display, layer, dst_rect, 0, src_rect, DISPMANX_PROTECTION_NONE, alpha, 0, 0) nativewindow = (GLint * 3)(self.dispman_element, w, h + 1) bcm.vc_dispmanx_update_submit_sync(self.dispman_update) self.nw_p = ctypes.pointer(nativewindow) ### NB changing the argtypes to allow passing of bcm native window is ### deeply unsatisfactory. But xlib defines Window as c_ulong and ctypes ### isn't happy about a pointer being cast to an int openegl.eglCreateWindowSurface.argtypes = [ EGLDisplay, EGLConfig, POINTER((GLint * 3)), EGLint ] self.surface = openegl.eglCreateWindowSurface( self.display, self.config, self.nw_p, 0) elif pi3d.USE_PYGAME: import pygame flags = pygame.OPENGL wsize = (w, h) if w == self.width and h == self.height: # i.e. full screen flags = pygame.FULLSCREEN | pygame.OPENGL wsize = (0, 0) if self.display_config & DISPLAY_CONFIG_NO_RESIZE: flags |= pygame.RESIZABLE if self.display_config & DISPLAY_CONFIG_NO_FRAME: flags |= pygame.NOFRAME if self.display_config & DISPLAY_CONFIG_FULLSCREEN: flags |= pygame.FULLSCREEN elif self.display_config & DISPLAY_CONFIG_MAXIMIZED: flags |= pygame.FULLSCREEN wsize = (0, 0) self.width, self.height = w, h self.d = pygame.display.set_mode(wsize, flags) self.window = pygame.display.get_wm_info()["window"] self.surface = openegl.eglCreateWindowSurface( self.display, self.config, self.window, 0) else: # work on basis it's X11 # Set some WM info self.root = xlib.XRootWindowOfScreen(self.screen) if self.use_glx: # For drawing on X window with transparent background numfbconfigs = c_int() VisData = c_ints( (glx.GLX_RENDER_TYPE, glx.GLX_RGBA_BIT, glx.GLX_DRAWABLE_TYPE, glx.GLX_WINDOW_BIT, glx.GLX_DOUBLEBUFFER, True, glx.GLX_RED_SIZE, 8, glx.GLX_GREEN_SIZE, 8, glx.GLX_BLUE_SIZE, 8, glx.GLX_ALPHA_SIZE, 8, glx.GLX_DEPTH_SIZE, 16, 0)) glx_screen = xlib.XDefaultScreen(self.d) fbconfigs = glx.glXChooseFBConfig(self.d, glx_screen, VisData, byref(numfbconfigs)) fbconfig = 0 for i in range(numfbconfigs.value): visual = glx.glXGetVisualFromFBConfig( self.d, fbconfigs[i]).contents if not visual: continue pict_format = glx.XRenderFindVisualFormat( self.d, visual.visual).contents if not pict_format: continue fbconfig = fbconfigs[i] if pict_format.direct.alphaMask > 0: break if not fbconfig: print("No matching FB config found") #/* Create a colormap - only needed on some X clients, eg. IRIX */ cmap = xlib.XCreateColormap(self.d, self.root, visual.visual, AllocNone) attr = xlib.XSetWindowAttributes() attr.colormap = cmap attr.background_pixmap = 0 attr.border_pixmap = 0 attr.border_pixel = 0 attr.event_mask = (StructureNotifyMask | EnterWindowMask | LeaveWindowMask | ExposureMask | ButtonPressMask | ButtonReleaseMask | OwnerGrabButtonMask | KeyPressMask | KeyReleaseMask) attr_mask = ( # CWBackPixmap| CWColormap | CWBorderPixel | CWEventMask) self.window = xlib.XCreateWindow(self.d, self.root, x, y, w, h, 0, visual.depth, 1, visual.visual, attr_mask, byref(attr)) else: # normal EGL created context self.window = xlib.XCreateSimpleWindow(self.d, self.root, x, y, w, h, 1, 0, 0) s = ctypes.create_string_buffer(b'WM_DELETE_WINDOW') self.WM_DELETE_WINDOW = ctypes.c_ulong( xlib.XInternAtom(self.d, s, 0)) # set window title title = ctypes.c_char_p(self.window_title) title_length = ctypes.c_int(len(self.window_title)) wm_name_atom = ctypes.c_ulong( xlib.XInternAtom(self.d, ctypes.create_string_buffer(b'WM_NAME'), 0)) string_atom = ctypes.c_ulong( xlib.XInternAtom(self.d, ctypes.create_string_buffer(b'STRING'), 0)) xlib.XChangeProperty(self.d, self.window, wm_name_atom, string_atom, 8, xlib.PropModeReplace, title, title_length) if (w == self.width and h == self.height) or (self.display_config & DISPLAY_CONFIG_FULLSCREEN): # set full-screen. Messy c function calls! wm_state = ctypes.c_ulong( xlib.XInternAtom(self.d, b'_NET_WM_STATE', 0)) fullscreen = ctypes.c_ulong( xlib.XInternAtom(self.d, b'_NET_WM_STATE_FULLSCREEN', 0)) fullscreen = ctypes.cast(ctypes.pointer(fullscreen), ctypes.c_char_p) XA_ATOM = 4 xlib.XChangeProperty(self.d, self.window, wm_state, XA_ATOM, 32, xlib.PropModeReplace, fullscreen, 1) self.width, self.height = w, h if self.display_config & DISPLAY_CONFIG_HIDE_CURSOR: black = xlib.XColor() black.red = 0 black.green = 0 black.blue = 0 noData = ctypes.c_char_p(bytes([0, 0, 0, 0, 0, 0, 0, 0])) bitmapNoData = xlib.XCreateBitmapFromData( self.d, self.window, noData, 8, 8) invisibleCursor = xlib.XCreatePixmapCursor( self.d, bitmapNoData, bitmapNoData, black, black, 0, 0) xlib.XDefineCursor(self.d, self.window, invisibleCursor) #TODO add functions to xlib for these window manager libx11 functions #self.window.set_wm_name('pi3d xlib window') #self.window.set_wm_icon_name('pi3d') #self.window.set_wm_class('draw', 'XlibExample') xlib.XSetWMProtocols(self.d, self.window, self.WM_DELETE_WINDOW, 1) #self.window.set_wm_hints(flags = Xutil.StateHint, # initial_state = Xutil.NormalState) #self.window.set_wm_normal_hints(flags = (Xutil.PPosition | Xutil.PSize # | Xutil.PMinSize), # min_width = 20, # min_height = 20) xlib.XSelectInput( self.d, self.window, KeyPressMask | KeyReleaseMask | ResizeRedirectMask) xlib.XMapWindow(self.d, self.window) #xlib.XMoveWindow(self.d, self.window, x, y) #TODO this has to happen later. Works after rendering first frame. Check when if self.use_glx: dummy = c_int() if not glx.glXQueryExtension(self.d, byref(dummy), byref(dummy)): print("OpenGL not supported by X server\n") dummy_glx_context = ctypes.cast(0, glx.GLXContext) self.render_context = glx.glXCreateNewContext( self.d, fbconfig, glx.GLX_RGBA_TYPE, dummy_glx_context, True) if not self.render_context: print("Failed to create a GL context\n") if not glx.glXMakeContextCurrent( self.d, self.window, self.window, self.render_context): print("glXMakeCurrent failed for window\n") else: self.surface = openegl.eglCreateWindowSurface( self.display, self.config, self.window, 0) if not self.use_glx: assert self.surface != EGL_NO_SURFACE and self.surface is not None r = openegl.eglMakeCurrent(self.display, self.surface, self.surface, self.context) assert r #Create viewport opengles.glViewport(GLint(0), GLint(0), GLsizei(w), GLsizei(h))
def draw(self, shape=None, M=None, unif=None, shader=None, textures=None, ntl=None, shny=None, fullset=True): """Draw this Buffer, called by the parent Shape.draw() Keyword arguments: *shape* Shape object this Buffer belongs to, has to be passed at draw to avoid circular reference *shader* Shader object *textures* array of Texture objects *ntl* multiple for tiling normal map which can be less than or greater than 1.0. 0.0 disables the normal mapping, float *shiny* how strong to make the reflection 0.0 to 1.0, float """ self.load_opengl() shader = shader or self.shader or shape.shader or Shader.instance() shader.use() opengles.glUniformMatrix4fv(shader.unif_modelviewmatrix, GLsizei(3), GLboolean(0), M.ctypes.data) opengles.glUniform3fv(shader.unif_unif, GLsizei(20), unif) textures = textures or self.textures if ntl is not None: self.unib[0] = ntl if shny is not None: self.unib[1] = shny self._select() opengles.glVertexAttribPointer(shader.attr_vertex, GLint(3), GL_FLOAT, GLboolean(0), self.N_BYTES, 0) opengles.glEnableVertexAttribArray(shader.attr_vertex) if self.N_BYTES > 12: opengles.glVertexAttribPointer(shader.attr_normal, GLint(3), GL_FLOAT, GLboolean(0), self.N_BYTES, 12) opengles.glEnableVertexAttribArray(shader.attr_normal) if self.N_BYTES > 24: opengles.glVertexAttribPointer(shader.attr_texcoord, GLint(2), GL_FLOAT, GLboolean(0), self.N_BYTES, 24) opengles.glEnableVertexAttribArray(shader.attr_texcoord) opengles.glDisable(GL_BLEND) self.unib[2] = 0.6 for t, texture in enumerate(textures): if (self.disp.last_textures[t] != texture or self.disp.last_shader != shader or self.disp.offscreen_tex): # very slight speed increase for sprites opengles.glActiveTexture(GL_TEXTURE0 + t) assert texture.tex(), 'There was an empty texture in your Buffer.' opengles.glBindTexture(GL_TEXTURE_2D, texture.tex()) opengles.glUniform1i(shader.unif_tex[t], GLint(t)) self.disp.last_textures[t] = texture if texture.blend: # i.e. if any of the textures set to blend then all will for this shader. self.unib[2] = 0.05 if self.unib[2] != 0.6 or shape.unif[13] < 1.0 or shape.unif[14] < 1.0: #use unib[2] as flag to indicate if any Textures to be blended #needs to be done outside for..textures so materials can be transparent opengles.glEnable(GL_BLEND) self.unib[2] = 0.05 self.disp.last_shader = shader opengles.glUniform3fv(shader.unif_unib, GLsizei(5), self.unib) opengles.glEnable(GL_DEPTH_TEST) # TODO find somewhere more efficient to do this opengles.glDrawElements(self.draw_method, GLsizei(self.ntris * 3), GL_UNSIGNED_SHORT, 0)
def _prepare(self) -> None: self.should_prepare = False import numpy as np # pi3d has to be imported in the same thread that it will draw in # Thus, import it here instead of at the top of the file import pi3d from pi3d.util.OffScreenTexture import OffScreenTexture from pi3d.constants import ( opengles, GL_CLAMP_TO_EDGE, GL_ALWAYS, ) # used for reimplementing the draw call with instancing from pi3d.constants import ( GLsizei, GLint, GLboolean, GL_FLOAT, GLuint, GL_ARRAY_BUFFER, GL_STATIC_DRAW, GLfloat, ) from PIL import Image # Setup display and initialise pi3d self.display = pi3d.Display.create( w=self.width, h=self.height, window_title="Raveberry" ) # error 0x500 after Display create # error = opengles.glGetError() # Set a pink background color so mistakes are clearly visible # self.display.set_background(1, 0, 1, 1) self.display.set_background(0, 0, 0, 1) # print OpenGL Version, useful for debugging # import ctypes # def print_char_p(addr): # g = (ctypes.c_char*32).from_address(addr) # i = 0 # while True: # c = g[i] # if c == b'\x00': # break # sys.stdout.write(c.decode()) # i += 1 # sys.stdout.write('\n') # print_char_p(opengles.glGetString(GL_VERSION)) # Visualization is split into five parts: # The background, the particles, the spectrum, the logo and after effects. # * The background is a vertical gradient that cycles through HSV color space, # speeding up with strong bass. # * Particles are multiple sprites that are created at a specified x,y-coordinate # and fly towards the camera. # Due to the projection into screenspace they seem to move away from the center. # * The spectrum is a white circle that represents the fft-transformation of the # currently played audio. It is smoothed to avoid strong spikes. # * The logo is a black circle on top of the spectrum containing the logo. # * After effects add a vignette. # Each of these parts is represented with pi3d Shapes. # They have their own shader and are ordered on the z-axis to ensure correct overlapping. background_shader = pi3d.Shader( os.path.join(settings.BASE_DIR, "core/lights/circle/background") ) self.background = pi3d.Sprite(w=2, h=2) self.background.set_shader(background_shader) self.background.positionZ(0) self.particle_shader = pi3d.Shader( os.path.join(settings.BASE_DIR, "core/lights/circle/particle") ) # create one sprite for all particles self.particle_sprite = pi3d.Sprite(w=self.PARTICLE_SIZE, h=self.PARTICLE_SIZE) self.particle_sprite.set_shader(self.particle_shader) self.particle_sprite.positionZ(0) # this array containes the position and speed for all particles particles = self._initial_particles() # This part was modified from https://learnopengl.com/Advanced-OpenGL/Instancing self.instance_vbo = GLuint() opengles.glGenBuffers(GLsizei(1), ctypes.byref(self.instance_vbo)) opengles.glBindBuffer(GL_ARRAY_BUFFER, self.instance_vbo) particles_raw = particles.ctypes.data_as(ctypes.POINTER(GLfloat)) opengles.glBufferData( GL_ARRAY_BUFFER, particles.nbytes, particles_raw, GL_STATIC_DRAW ) opengles.glBindBuffer(GL_ARRAY_BUFFER, GLuint(0)) attr_particle = opengles.glGetAttribLocation( self.particle_shader.program, b"particle" ) opengles.glEnableVertexAttribArray(attr_particle) opengles.glBindBuffer(GL_ARRAY_BUFFER, self.instance_vbo) opengles.glVertexAttribPointer( attr_particle, GLint(4), GL_FLOAT, GLboolean(0), 0, 0 ) opengles.glBindBuffer(GL_ARRAY_BUFFER, GLuint(0)) opengles.glVertexAttribDivisor(attr_particle, GLuint(1)) spectrum_shader = pi3d.Shader( os.path.join(settings.BASE_DIR, "core/lights/circle/spectrum") ) # use the ratio to compute small sizes for the sprites ratio = self.width / self.height self.spectrum = pi3d.Sprite(w=2 / ratio, h=2) self.spectrum.set_shader(spectrum_shader) self.spectrum.positionZ(0) # initialize the spectogram history with zeroes self.fft = np.zeros( (self.FFT_HIST, self.cava.bars - 2 * self.SPECTRUM_CUT), dtype=np.uint8 ) logo_shader = pi3d.Shader( os.path.join(settings.BASE_DIR, "core/lights/circle/logo") ) self.logo = pi3d.Sprite(w=1.375 / ratio, h=1.375) self.logo.set_shader(logo_shader) self.logo.positionZ(0) logo_image = Image.open( os.path.join(settings.STATIC_ROOT, "graphics/raveberry_square.png") ) self.logo_array = np.frombuffer(logo_image.tobytes(), dtype=np.uint8) self.logo_array = self.logo_array.reshape( (logo_image.size[1], logo_image.size[0], 3) ) # add space for the spectrum self.logo_array = np.concatenate( ( self.logo_array, np.zeros((self.FFT_HIST, logo_image.size[0], 3), dtype=np.uint8), ), axis=0, ) # add alpha channel self.logo_array = np.concatenate( ( self.logo_array, np.ones( (self.logo_array.shape[0], self.logo_array.shape[1], 1), dtype=np.uint8, ), ), axis=2, ) # In order to save memory, the logo and the spectrum share one texture. # The upper 256x256 pixels are the raveberry logo. # Below are 256xFFT_HIST pixels for the spectrum. # The lower part is periodically updated every frame while the logo stays static. self.dynamic_texture = pi3d.Texture(self.logo_array) # Prevent interpolation from opposite edge self.dynamic_texture.m_repeat = GL_CLAMP_TO_EDGE self.spectrum.set_textures([self.dynamic_texture]) self.logo.set_textures([self.dynamic_texture]) after_shader = pi3d.Shader( os.path.join(settings.BASE_DIR, "core/lights/circle/after") ) self.after = pi3d.Sprite(w=2, h=2) self.after.set_shader(after_shader) self.after.positionZ(0) # create an OffscreenTexture to allow scaling. # By first rendering into a smaller Texture a lot of computation is saved. # This OffscreenTexture is then drawn at the end of the draw loop. self.post = OffScreenTexture("scale") self.post_sprite = pi3d.Sprite(w=2, h=2) post_shader = pi3d.Shader( os.path.join(settings.BASE_DIR, "core/lights/circle/scale") ) self.post_sprite.set_shader(post_shader) self.post_sprite.set_textures([self.post]) self.total_bass = 0 self.last_loop = time.time() self.time_elapsed = 0 opengles.glDepthFunc(GL_ALWAYS)
def draw(self) -> None: import numpy as np from scipy.ndimage.filters import gaussian_filter from pi3d.Camera import Camera from pi3d.constants import ( opengles, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GLsizei, GLboolean, GLint, GL_FLOAT, GL_ARRAY_BUFFER, GL_UNSIGNED_SHORT, GL_TEXTURE_2D, GL_UNSIGNED_BYTE, ) time_logging = False if self.should_prepare: self._prepare() if self.lights.alarm_program.factor != -1: self.alarm_factor = max(0.001, self.lights.alarm_program.factor) else: self.alarm_factor = 0 then = time.time() self.display.loop_running() now = self.display.time self.time_delta = now - self.last_loop self.last_loop = now self.time_elapsed += self.time_delta if time_logging: print(f"{time.time() - then} main loop") then = time.time() # use a sliding window to smooth the spectrum with a gauss function # truncating does not save significant time (~3% for this step) # new_frame = np.array(self.cava.current_frame, dtype="float32") new_frame = gaussian_filter(self.cava.current_frame, sigma=1.5, mode="nearest") new_frame = new_frame[self.SPECTRUM_CUT : -self.SPECTRUM_CUT] new_frame = -0.5 * new_frame ** 3 + 1.5 * new_frame new_frame *= 255 current_frame = new_frame if time_logging: print(f"{time.time() - then} spectrum smoothing") then = time.time() # Value used for circle shake and background color cycle # select the first few values and compute their average bass_elements = math.ceil(self.BASS_MAX * self.cava.bars) self.bass_value = sum(current_frame[0:bass_elements]) / bass_elements / 255 self.bass_value = max(self.bass_value, self.alarm_factor) self.total_bass = self.total_bass + self.bass_value # the fraction of time that there was bass self.bass_fraction = self.total_bass / self.time_elapsed / self.lights.UPS self.uniform_values = { 48: self.width / self.scale, 49: self.height / self.scale, 50: self.scale, 51: self.FFT_HIST, 52: self.NUM_PARTICLES, 53: self.PARTICLE_SPAWN_Z, 54: self.time_elapsed, 55: self.time_delta, 56: self.alarm_factor, 57: self.bass_value, 58: self.total_bass, 59: self.bass_fraction, } # start rendering to the smaller OffscreenTexture # we decrease the size of the texture so it only allocates that much memory # otherwise it would use as much as the displays size, negating its positive effect self.post.ix = int(self.post.ix / self.scale) self.post.iy = int(self.post.iy / self.scale) opengles.glViewport( GLint(0), GLint(0), GLsizei(int(self.width / self.scale)), GLsizei(int(self.height / self.scale)), ) self.post._start() self.post.ix = self.width self.post.iy = self.height self._set_unif(self.background, [48, 49, 54, 56, 58]) self.background.draw() if time_logging: print(f"{time.time() - then} background draw") then = time.time() # enable additive blending so the draw order of overlapping particles does not matter opengles.glBlendFunc(1, 1) self._set_unif(self.particle_sprite, [53, 54, 59]) # copied code from pi3d.Shape.draw() # we don't need modelmatrices, normals ord textures and always blend self.particle_sprite.load_opengl() camera = Camera.instance() if not camera.mtrx_made: camera.make_mtrx() self.particle_sprite.MRaw = self.particle_sprite.tr1 self.particle_sprite.M[0, :, :] = self.particle_sprite.MRaw[:, :] self.particle_sprite.M[1, :, :] = np.dot( self.particle_sprite.MRaw, camera.mtrx )[:, :] # Buffer.draw() buf = self.particle_sprite.buf[0] buf.load_opengl() shader = buf.shader shader.use() opengles.glUniformMatrix4fv( shader.unif_modelviewmatrix, GLsizei(2), GLboolean(0), self.particle_sprite.M.ctypes.data, ) opengles.glUniform3fv(shader.unif_unif, GLsizei(20), self.particle_sprite.unif) buf._select() opengles.glVertexAttribPointer( shader.attr_vertex, GLint(3), GL_FLOAT, GLboolean(0), buf.N_BYTES, 0 ) opengles.glEnableVertexAttribArray(shader.attr_vertex) opengles.glVertexAttribPointer( shader.attr_texcoord, GLint(2), GL_FLOAT, GLboolean(0), buf.N_BYTES, 24 ) opengles.glEnableVertexAttribArray(shader.attr_texcoord) buf.disp.last_shader = shader opengles.glUniform3fv(shader.unif_unib, GLsizei(5), buf.unib) opengles.glBindBuffer(GL_ARRAY_BUFFER, self.instance_vbo) opengles.glDrawElementsInstanced( buf.draw_method, GLsizei(buf.ntris * 3), GL_UNSIGNED_SHORT, 0, self.NUM_PARTICLES, ) # restore normal blending opengles.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) if time_logging: print(f"{time.time() - then} particle draw") then = time.time() # roll the history one further, insert the current one. # we use a texture with four channels eventhough we only need one, refer to this post: # https://community.khronos.org/t/updating-textures-per-frame/75020/3 # basically the gpu converts it anyway, so other formats would be slower history = np.zeros( (self.FFT_HIST, self.cava.bars - 2 * self.SPECTRUM_CUT, 4), dtype="uint8" ) self.fft = np.roll(self.fft, 1, 0) self.fft[0] = current_frame history[:, :, 0] = self.fft if time_logging: print(f"{time.time() - then} spectrum roll") then = time.time() # change the spectrum part of the texture (the lower 256xFFT_HIST pixels) opengles.glBindTexture(GL_TEXTURE_2D, self.dynamic_texture._tex) iformat = self.dynamic_texture._get_format_from_array( history, self.dynamic_texture.i_format ) opengles.glTexSubImage2D( GL_TEXTURE_2D, 0, 0, self.dynamic_texture.ix, history.shape[1], history.shape[0], iformat, GL_UNSIGNED_BYTE, history.ctypes.data_as(ctypes.POINTER(ctypes.c_ubyte)), ) if time_logging: print(f"{time.time() - then} glTexImage2D") then = time.time() self._set_unif(self.spectrum, [48, 49, 51, 52, 53, 54, 55, 57, 58]) self.spectrum.draw() if time_logging: print(f"{time.time() - then} spectrum draw") then = time.time() self._set_unif(self.logo, [48, 49, 51, 54, 57, 58]) self.logo.draw() if time_logging: print(f"{time.time() - then} logo draw") then = time.time() self._set_unif(self.after, [48, 49, 54, 57]) self.after.draw() if time_logging: print(f"{time.time() - then} after draw") then = time.time() self.post._end() opengles.glViewport( GLint(0), GLint(0), GLsizei(self.width), GLsizei(self.height) ) self._set_unif(self.post_sprite, [50]) self.post_sprite.draw() if time_logging: print(f"{time.time() - then} post draw") print(f"scale: {self.scale}") print("=====")