Source code for pi3d.Display

from __future__ import absolute_import, division, print_function, unicode_literals

from ctypes import c_float, byref

import time
import threading
import traceback
import platform
import logging

import pi3d
from pi3d.util.DisplayOpenGL import DisplayOpenGL
from pi3d.constants import (openegl, opengles, PLATFORM, PLATFORM_ANDROID,
          PLATFORM_PI, PLATFORM_WINDOWS, STARTUP_MESSAGE, DISPLAY_CONFIG_DEFAULT,
          GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GLclampf, GLboolean, GLsizei)
if PLATFORM == PLATFORM_WINDOWS:
  import pygame
elif PLATFORM != PLATFORM_PI and PLATFORM != PLATFORM_ANDROID:
  from pyxlib.x import KeyPress, KeyRelease, ClientMessage, ResizeRequest
  from pyxlib import xlib

LOGGER = logging.getLogger(__name__)

ALLOW_MULTIPLE_DISPLAYS = False
RAISE_EXCEPTIONS = True
MARK_CAMERA_CLEAN_ON_EACH_LOOP = True

DEFAULT_FOV = 45.0
DEFAULT_DEPTH = 24
DEFAULT_SAMPLES = 0
DEFAULT_NEAR = 1.0
DEFAULT_FAR = 1000.0
WIDTH = 0
HEIGHT = 0

if PLATFORM == PLATFORM_ANDROID:
  from kivy.app import App
  from kivy.uix.floatlayout import FloatLayout
  from kivy.clock import Clock

  class Pi3dScreen(FloatLayout):
    def __init__(self, *args, **kwargs):
      super(Pi3dScreen, self).__init__()
      self.TAP_TM = 0.15
      self.TAP_GAP = 1.0
      self.moved = False
      self.tapped = False
      self.double_tapped = False
      self.last_down = 0.0
      self.last_last_down = 0.0
      self.touch = None
      self.previous_touch = None

    def update(self, dt):
      pass

    def on_touch_down(self, touch):
      touch.ud['down'] = True #needed for keeping track of 'other' touch location
      self.last_last_down = self.last_down
      self.last_down = time.time()
      self.previous_touch = self.touch
      self.touch = touch

    def on_touch_move(self, touch):
      self.moved = True
      self.touch = touch

    def on_touch_up(self, touch):
      tm_now = time.time()
      if (tm_now - self.last_down) < self.TAP_TM: #this was a tap
        if (tm_now - self.last_last_down) < self.TAP_GAP : #and near enough to be double
          self.double_tapped = True
          self.tapped = False
        else:
          self.tapped = True
          self.double_tapped = False
      touch.ud['down'] = False

  class Pi3dApp(App):
    frames_per_second = 60.0
    def set_loop(self, loop_function):
      self.loop_function = loop_function

    def build(self):
      self.screen = Pi3dScreen()
      Clock.schedule_interval(self.loop_function, 1.0 / self.frames_per_second)
      return self.screen

[docs]class Display(object): """This is the central control object of the pi3d system and an instance must be created before some of the other class methods are called. """ INSTANCE = None """The current unique instance of Display.""" def __init__(self, tkwin=None, use_pygame=False): """ Constructs a raw Display. Use pi3d.Display.create to create an initialized Display. *tkwin* An optional Tk window. *use_pygame* Flag to opt for pygame """ if Display.INSTANCE is not None: assert ALLOW_MULTIPLE_DISPLAYS LOGGER.warning('A second instance of Display was created') else: Display.INSTANCE = self self.tkwin = tkwin if PLATFORM == PLATFORM_PI: use_pygame = False elif use_pygame or PLATFORM == PLATFORM_WINDOWS: try: import pygame use_pygame = True # for Windows except ImportError: LOGGER.warning('Do you need to install pygame?') use_pygame = False pi3d.USE_PYGAME = use_pygame self.sprites = [] self.sprites_to_load = set() self.sprites_to_unload = set() self.tidy_needed = False self.textures_dict = {} self.vbufs_dict = {} self.ebufs_dict = {} self.last_shader = None self.last_textures = [None for _ in range(8)] # 8 is max no. texture2D on broadcom GPU self.external_mouse = None self.offscreen_tex = False # used in Buffer.draw() to force reload of textures if (PLATFORM != PLATFORM_PI and PLATFORM != PLATFORM_ANDROID and not pi3d.USE_PYGAME): self.event_list = [] self.ev = xlib.XEvent() elif PLATFORM == PLATFORM_ANDROID: self.android = Pi3dApp() self.opengl = DisplayOpenGL() self.max_width, self.max_height = self.opengl.width, self.opengl.height self.first_time = True self.is_running = True self.lock = threading.RLock() self.was_resized = False LOGGER.debug(STARTUP_MESSAGE)
[docs] def loop_running(self): """*loop_running* is the main event loop for the Display. Most pi3d code will look something like this:: DISPLAY = Display.create() # Initialize objects and variables here. # ... while DISPLAY.loop_running(): # Update the frame, using DISPLAY.time for the current time. # ... # Check for quit, then call DISPLAY.stop. if some_quit_condition(): DISPLAY.stop() ``Display.loop_running()`` **must** be called on the main Python thread, or else white screens and program crashes are likely. The Display loop can run in two different modes - *free* or *framed*. If ``DISPLAY.frames_per_second`` is empty or 0 then the loop runs *free* - when it finishes one frame, it immediately starts working on the next frame. If ``Display.frames_per_second`` is a positive number then the Display is *framed* - when the Display finishes one frame before the next frame_time, it waits till the next frame starts. A free Display gives the highest frame rate, but it will also consume more CPU, to the detriment of other threads or other programs. There is also the significant drawback that the framerate will fluctuate as the numbers of CPU cycles consumed per loop, resulting in jerky motion and animations. A framed Display has a consistent if smaller number of frames, and also allows for potentially much smoother motion and animation. The ability to throttle down the number of frames to conserve CPU cycles is essential for programs with other important threads like audio. ``Display.frames_per_second`` can be set at construction in ``Display.create`` or changed on-the-fly during the execution of the program. If ``Display.frames_per_second`` is set too high, the Display doesn't attempt to "catch up" but simply runs freely. """ if self.is_running: if self.first_time: self.time = time.time() self.first_time = False else: self._loop_end() # Finish the previous loop. self._loop_begin() else: self._loop_end() self.destroy() return self.is_running
[docs] def resize(self, x=0, y=0, w=0, h=0): """Reshape the window with the given coordinates.""" if w <= 0: w = self.max_width if h <= 0: h = self.max_height self.width = w self.height = h self.left = x self.top = y self.right = x + w self.bottom = y + h self.opengl.resize(x, y, w, h, self.layer) self.was_resized = True
[docs] def change_layer(self, layer=0): self.layer = layer self.opengl.change_layer(layer)
[docs] def add_sprites(self, *sprites): """Add one or more sprites to this Display.""" with self.lock: self.sprites_to_load.update(sprites)
[docs] def remove_sprites(self, *sprites): """Remove one or more sprites from this Display.""" with self.lock: self.sprites_to_unload.update(sprites)
[docs] def stop(self): """Stop the Display.""" self.is_running = False
[docs] def destroy(self): """Destroy the current Display and reset Display.INSTANCE.""" self._tidy() self.stop() try: self.opengl.destroy(self) except: pass if self.external_mouse: try: self.external_mouse.stop() except: pass try: self.mouse.stop() except: pass try: self.tkwin.destroy() except: pass Display.INSTANCE = None if pi3d.USE_PYGAME: try: import pygame # NB seems to be needed on some setups (64 bit anaconda windows!) pygame.quit() except: pass
[docs] def clear(self): """Clear the Display.""" # opengles.glBindFramebuffer(GL_FRAMEBUFFER,0) opengles.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
[docs] def set_background(self, r, g, b, alpha): """Set the Display background. **NB the actual drawing of the background happens during the rendering of the framebuffer by the shader so if no draw() is done by anything during each Display loop the screen will remain black** If you want to see just the background you will have to draw() something out of view (i.e. behind) the Camera. *r, g, b* Color values for the display *alpha* Opacity of the color. An alpha of 0 means a transparent background, an alpha of 1 means full opaque. """ if alpha < 1.0 and (not self.opengl.use_glx) and (not PLATFORM == PLATFORM_PI): LOGGER.warning("create Display with (...use_glx=True) for transparent background on x11 window. libGLX needs to be available") opengles.glClearColor(GLclampf(r), GLclampf(g), GLclampf(b), GLclampf(alpha)) opengles.glColorMask(GLboolean(1), GLboolean(1), GLboolean(1), GLboolean(alpha < 1.0))
# Switches off alpha blending with desktop (is there a bug in the driver?)
[docs] def mouse_position(self): """The current mouse position as a tuple.""" # TODO: add: Now deprecated in favor of pi3d.events if self.mouse: return self.mouse.position() elif self.tkwin: return self.tkwin.winfo_pointerxy() else: return -1, -1
def _loop_begin(self): # TODO(rec): check if the window was resized and resize it, removing # code from MegaStation to here. if pi3d.USE_PYGAME: import pygame # although done in __init__ ...python namespaces aarg!!! if pygame.event.get(pygame.QUIT): self.destroy() elif PLATFORM != PLATFORM_PI and PLATFORM != PLATFORM_ANDROID: n = xlib.XEventsQueued(self.opengl.d, xlib.QueuedAfterFlush) for _ in range(n): xlib.XNextEvent(self.opengl.d, self.ev) if self.ev.type == KeyPress or self.ev.type == KeyRelease: self.event_list.append(self.ev) elif self.ev.type == ClientMessage: if (self.ev.xclient.data.l[0] == self.opengl.WM_DELETE_WINDOW.value): self.destroy() elif self.ev.type == ResizeRequest: (self.width, self.height) = (self.ev.xresizerequest.width, self.ev.xresizerequest.height) opengles.glViewport(0, 0, self.width, self.height) self.was_resized = True self.clear() with self.lock: self.sprites_to_load, to_load = set(), self.sprites_to_load self.sprites.extend(to_load) self._for_each_sprite(lambda s: s.load_opengl(), to_load) if MARK_CAMERA_CLEAN_ON_EACH_LOOP: from pi3d.Camera import Camera #camera = Camera.instance() #if camera is not None: # camera.was_moved = False cameras = Camera.all_instances() if cameras is not None: for camera in cameras: camera.was_moved = False if self.tidy_needed: self._tidy() 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 _loop_end(self): if pi3d.USE_PYGAME: import pygame pygame.event.clear() with self.lock: self.sprites_to_unload, to_unload = set(), self.sprites_to_unload if to_unload: self.sprites = [s for s in self.sprites if s not in to_unload] t = time.time() self._for_each_sprite(lambda s: s.repaint(t)) self.swap_buffers() for sprite in to_unload: sprite.unload_opengl() if self.frames_per_second: delta = 1.0 / self.frames_per_second - (time.time() - self.time) if delta > 0: time.sleep(delta) self.time = time.time() def _for_each_sprite(self, function, sprites=None): if sprites is None: sprites = self.sprites for s in sprites: try: function(s) except: LOGGER.error(traceback.format_exc()) if RAISE_EXCEPTIONS: raise def __del__(self): try: self.destroy() except: pass # often something has already been deleted leading to various None objects
[docs] def swap_buffers(self): self.opengl.swap_buffers()
[docs]def create(x=None, y=None, w=None, h=None, near=None, far=None, fov=DEFAULT_FOV, depth=DEFAULT_DEPTH, background=None, tk=False, window_title='', window_parent=None, mouse=False, frames_per_second=None, samples=DEFAULT_SAMPLES, use_pygame=False, layer=0, display_config=DISPLAY_CONFIG_DEFAULT, use_glx=False): """ Creates a pi3d Display. *x* Left x coordinate of the display. If None, defaults to the x coordinate of the tkwindow parent, if any. *y* Top y coordinate of the display. If None, defaults to the y coordinate of the tkwindow parent, if any. *w* Width of the display. If None, full the width of the screen. *h* Height of the display. If None, full the height of the screen. *near* This will be used for the default instance of Camera *near* plane *far* This will be used for the default instance of Camera *far* plane *fov* Used to define the Camera lens field of view *depth* The bit depth of the display - must be 8, 16 or 24. *background* r,g,b,alpha (opacity) *tk* Do we use the tk windowing system? *window_title* A window title for tk windows only. *window_parent* An optional tk parent window. *mouse* Automatically create a Mouse. *frames_per_second* Maximum frames per second to render (None means "free running"). *samples* EGL_SAMPLES default 0, set to 4 for improved anti-aliasing *use_pygame* To use pygame for display surface, mouse and keyboard - as per windows This almost certainly would conflict if attempting to use in combination with tk=True. Default False *layer* display layer height - used by dispmanx on Raspberry Pi only. -128 will move the pi3d window behind the X11 desktop *display_config* Configuration of display - See pi3d.constants for DISPLAY_CONFIG options """ if tk: #NB this happens before Display created so use_pygame will not work on linux if PLATFORM != PLATFORM_PI and PLATFORM != PLATFORM_ANDROID: #just use python-xlib same as non-tk but need dummy behaviour from pi3d.Keyboard import Keyboard class DummyTkWin(object): def __init__(self): self.tkKeyboard = Keyboard() self.ev = "" self.key = "" self.winx, self.winy = 0, 0 self.width, self.height = 1920, 1180 self.event_list = [] def update(self): if PLATFORM == PLATFORM_WINDOWS or pi3d.USE_PYGAME: #uses pygame UI k = self.tkKeyboard.read() if k == -1: self.key = "" self.ev = "" else: if k == 27: self.key = "Escape" else: self.key = chr(k) self.ev = "key" else: self.key = self.tkKeyboard.read_code() if self.key == "": self.ev = "" else: self.ev = "key" tkwin = DummyTkWin() x = x or 0 y = y or 0 else: from pi3d.util import TkWin if not (w and h): # TODO: how do we do full-screen in tk? #LOGGER.error('Can't compute default window size when using tk') #raise Exception # ... just force full screen - TK will automatically fit itself into the screen w = 1920 h = 1180 if background is not None: bg_i = [int(i * 255) for i in background] bg = '#{:02X}{:02X}{:02X}'.format(bg_i[0], bg_i[1], bg_i[2]) else: bg = '#000000' tkwin = TkWin.TkWin(window_parent, window_title, w, h, bg) tkwin.update() w = tkwin.winfo_width() h = tkwin.winfo_height() if x is None: x = tkwin.winx if y is None: y = tkwin.winy else: tkwin = None x = x or 0 y = y or 0 display = Display(tkwin, use_pygame) if (w or 0) <= 0: w = display.max_width - 2 * x if w <= 0: w = display.max_width if (h or 0) <= 0: h = display.max_height - 2 * y if h <= 0: h = display.max_height LOGGER.debug('Display size is w=%d, h=%d', w, h) display.frames_per_second = frames_per_second or 0 if near is None: near = DEFAULT_NEAR if far is None: far = DEFAULT_FAR display.width = w display.height = h display.near = near display.far = far display.fov = fov display.left = x display.top = y display.right = x + w display.bottom = y + h display.layer = layer display.opengl.create_display(x, y, w, h, depth=depth, samples=samples, layer=layer, display_config=display_config, window_title=window_title, use_glx=use_glx) if PLATFORM == PLATFORM_ANDROID: display.width = display.right = display.max_width = display.opengl.width #not available until after create_display display.height = display.bottom = display.max_height = display.opengl.height display.top = display.bottom = 0 if frames_per_second: display.android.frames_per_second = frames_per_second display.frames_per_second = 0 #to avoid clash between two systems! display.mouse = None if mouse: from pi3d.Mouse import Mouse display.mouse = Mouse(width=w, height=h, restrict=False) display.mouse.start() if background is not None: display.set_background(*background) return display