diff options
Diffstat (limited to 'silx/gui/plot3d/scene/window.py')
-rw-r--r-- | silx/gui/plot3d/scene/window.py | 421 |
1 files changed, 421 insertions, 0 deletions
diff --git a/silx/gui/plot3d/scene/window.py b/silx/gui/plot3d/scene/window.py new file mode 100644 index 0000000..3c63c7a --- /dev/null +++ b/silx/gui/plot3d/scene/window.py @@ -0,0 +1,421 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2015-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""This module provides a class for Viewports rendering on the screen. + +The :class:`Window` renders a list of Viewports in the current framebuffer. +The rendering can be performed in an off-screen framebuffer that is only +updated when the scene has changed and not each time Qt is requiring a repaint. + +The :class:`Context` and :class:`ContextGL2` represent the operating system +OpenGL context and handle OpenGL resources. +""" + +from __future__ import absolute_import, division, unicode_literals + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "10/01/2017" + + +import weakref +import numpy + +from ..._glutils import gl +from ... import _glutils + +from . import event + + +class Context(object): + """Correspond to an operating system OpenGL context. + + User should NEVER use an instance of this class beyond the method + it is passed to as an argument (i.e., do not keep a reference to it). + + :param glContextHandle: System specific OpenGL context handle. + """ + + def __init__(self, glContextHandle): + self._context = glContextHandle + self._isCurrent = False + self._devicePixelRatio = 1.0 + + @property + def isCurrent(self): + """Whether this OpenGL context is the current one or not.""" + return self._isCurrent + + def setCurrent(self, isCurrent=True): + """Set the state of the OpenGL context to reflect OpenGL state. + + This should not be called from the scene graph, only in the + wrapper that handle the OpenGL context to reflect its state. + + :param bool isCurrent: The state of the system OpenGL context. + """ + self._isCurrent = bool(isCurrent) + + @property + def devicePixelRatio(self): + """Ratio between device and device independent pixels (float) + + This is useful for font rendering. + """ + return self._devicePixelRatio + + @devicePixelRatio.setter + def devicePixelRatio(self, ratio): + assert ratio > 0 + self._devicePixelRatio = float(ratio) + + def __enter__(self): + self.setCurrent(True) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.setCurrent(False) + + @property + def glContext(self): + """The handle to the OpenGL context provided by the system.""" + return self._context + + def cleanGLGarbage(self): + """This is releasing OpenGL resource that are no longer used.""" + pass + + +class ContextGL2(Context): + """Handle a system GL2 context. + + User should NEVER use an instance of this class beyond the method + it is passed to as an argument (i.e., do not keep a reference to it). + + :param glContextHandle: System specific OpenGL context handle. + """ + def __init__(self, glContextHandle): + super(ContextGL2, self).__init__(glContextHandle) + + self._programs = {} # GL programs already compiled + self._vbos = {} # GL Vbos already set + self._vboGarbage = [] # Vbos waiting to be discarded + + # programs + + def prog(self, vertexShaderSrc, fragmentShaderSrc): + """Cache program within context. + + WARNING: No clean-up. + """ + assert self.isCurrent + key = vertexShaderSrc, fragmentShaderSrc + prog = self._programs.get(key, None) + if prog is None: + prog = _glutils.Program(vertexShaderSrc, fragmentShaderSrc) + self._programs[key] = prog + return prog + + # VBOs + + def makeVbo(self, data=None, sizeInBytes=None, + usage=None, target=None): + """Create a VBO in this context with the data. + + Current limitations: + + - One array per VBO + - Do not support sharing VertexBuffer across VboAttrib + + Automatically discards the VBO when the returned + :class:`VertexBuffer` istance is deleted. + + :param numpy.ndarray data: 2D array of data to store in VBO or None. + :param int sizeInBytes: Size of the VBO or None. + It should be <= data.nbytes if both are given. + :param usage: OpenGL usage define in VertexBuffer._USAGES. + :param target: OpenGL target in VertexBuffer._TARGETS. + :return: The VertexBuffer created in this context. + """ + assert self.isCurrent + vbo = _glutils.VertexBuffer(data, sizeInBytes, usage, target) + vboref = weakref.ref(vbo, self._deadVbo) + # weakref is hashable as far as target is + self._vbos[vboref] = vbo.name + return vbo + + def makeVboAttrib(self, data, usage=None, target=None): + """Create a VBO from data and returns the associated VBOAttrib. + + Automatically discards the VBO when the returned + :class:`VBOAttrib` istance is deleted. + + :param numpy.ndarray data: 2D array of data to store in VBO or None. + :param usage: OpenGL usage define in VertexBuffer._USAGES. + :param target: OpenGL target in VertexBuffer._TARGETS. + :returns: A VBOAttrib instance created in this context. + """ + assert self.isCurrent + vbo = self.makeVbo(data, usage=usage, target=target) + + assert len(data.shape) <= 2 + dimension = 1 if len(data.shape) == 1 else data.shape[1] + + return _glutils.VertexBufferAttrib( + vbo, + type_=_glutils.numpyToGLType(data.dtype), + size=data.shape[0], + dimension=dimension, + offset=0, + stride=0) + + def _deadVbo(self, vboRef): + """Callback handling dead VBOAttribs.""" + vboid = self._vbos.pop(vboRef) + if self.isCurrent: + # Direct delete if context is active + gl.glDeleteBuffers(vboid) + else: + # Deferred VBO delete if context is not active + self._vboGarbage.append(vboid) + + def cleanGLGarbage(self): + """Delete OpenGL resources that are pending for destruction. + + This requires the associated OpenGL context to be active. + This is meant to be called before rendering. + """ + assert self.isCurrent + if self._vboGarbage: + vboids = self._vboGarbage + gl.glDeleteBuffers(vboids) + self._vboGarbage = [] + + +class Window(event.Notifier): + """OpenGL Framebuffer where to render viewports + + :param str mode: Rendering mode to use: + + - 'direct' to render everything for each render call + - 'framebuffer' to cache viewport rendering in a texture and + update the texture only when needed. + """ + + _position = numpy.array(((-1., -1., 0., 0.), + (1., -1., 1., 0.), + (-1., 1., 0., 1.), + (1., 1., 1., 1.)), + dtype=numpy.float32) + + _shaders = (""" + attribute vec4 position; + varying vec2 textureCoord; + + void main(void) { + gl_Position = vec4(position.x, position.y, 0., 1.); + textureCoord = position.zw; + } + """, + """ + uniform sampler2D texture; + varying vec2 textureCoord; + + void main(void) { + gl_FragColor = texture2D(texture, textureCoord); + gl_FragColor.a = 1.0; + } + """) + + def __init__(self, mode='framebuffer'): + super(Window, self).__init__() + self._dirty = True + self._size = 0, 0 + self._contexts = {} # To map system GL context id to Context objects + self._viewports = event.NotifierList() + self._viewports.addListener(self._updated) + self._framebufferid = 0 + self._framebuffers = {} # Cache of framebuffers + + assert mode in ('direct', 'framebuffer') + self._isframebuffer = mode == 'framebuffer' + + @property + def dirty(self): + """True if this object or any attached viewports is dirty.""" + for viewport in self._viewports: + if viewport.dirty: + return True + return self._dirty + + @property + def size(self): + """Size (width, height) of the window in pixels""" + return self._size + + @size.setter + def size(self, size): + w, h = size + size = int(w), int(h) + if size != self._size: + self._size = size + self._dirty = True + self.notify() + + @property + def shape(self): + """Shape (height, width) of the window in pixels. + + This is a convenient wrapper to the reverse of size. + """ + return self._size[1], self._size[0] + + @shape.setter + def shape(self, shape): + self.size = shape[1], shape[0] + + @property + def viewports(self): + """List of viewports to render in the corresponding framebuffer""" + return self._viewports + + @viewports.setter + def viewports(self, iterable): + self._viewports.removeListener(self._updated) + self._viewports = event.NotifierList(iterable) + self._viewports.addListener(self._updated) + self._updated(self) + + def _updated(self, source, *args, **kwargs): + self._dirty = True + self.notify(*args, **kwargs) + + framebufferid = property(lambda self: self._framebufferid, + doc="Framebuffer ID used to perform rendering") + + def grab(self, glcontext): + """Returns the raster of the scene as an RGB numpy array + + :returns: OpenGL scene RGB bitmap + :rtype: numpy.ndarray of uint8 of dimension (height, width, 3) + """ + height, width = self.shape + image = numpy.empty((height, width, 3), dtype=numpy.uint8) + + previousFramebuffer = gl.glGetInteger(gl.GL_FRAMEBUFFER_BINDING) + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.framebufferid) + gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) + gl.glReadPixels( + 0, 0, width, height, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, image) + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, previousFramebuffer) + + # glReadPixels gives bottom to top, + # while images are stored as top to bottom + image = numpy.flipud(image) + + return numpy.array(image, copy=False, order='C') + + def render(self, glcontext, devicePixelRatio): + """Perform the rendering of attached viewports + + :param glcontext: System identifier of the OpenGL context + :param float devicePixelRatio: + Ratio between device and device-independent pixels + """ + if glcontext not in self._contexts: + self._contexts[glcontext] = ContextGL2(glcontext) # New context + + with self._contexts[glcontext] as context: + context.devicePixelRatio = devicePixelRatio + if self._isframebuffer: + self._renderWithOffscreenFramebuffer(context) + else: + self._renderDirect(context) + + self._dirty = False + + def _renderDirect(self, context): + """Perform the direct rendering of attached viewports + + :param Context context: Object wrapping OpenGL context + """ + for viewport in self._viewports: + viewport.framebuffer = self.framebufferid + viewport.render(context) + viewport.resetDirty() + + def _renderWithOffscreenFramebuffer(self, context): + """Renders viewports in a texture and render this texture on screen. + + The texture is updated only if viewport or size has changed. + + :param ContextGL2 context: Object wrappign OpenGL context + """ + if self.dirty or context not in self._framebuffers: + # Need to redraw framebuffer content + + if (context not in self._framebuffers or + self._framebuffers[context].shape != self.shape): + # Need to rebuild framebuffer + + if context in self._framebuffers: + self._framebuffers[context].discard() + + fbo = _glutils.FramebufferTexture(gl.GL_RGBA, + shape=self.shape, + minFilter=gl.GL_NEAREST, + magFilter=gl.GL_NEAREST, + wrap=gl.GL_CLAMP_TO_EDGE) + self._framebuffers[context] = fbo + self._framebufferid = fbo.name + + # Render in framebuffer + with self._framebuffers[context]: + self._renderDirect(context) + + # Render framebuffer texture to screen + fbo = self._framebuffers[context] + height, width = fbo.shape + + program = context.prog(*self._shaders) + program.use() + + gl.glViewport(0, 0, width, height) + gl.glDisable(gl.GL_BLEND) + gl.glDisable(gl.GL_DEPTH_TEST) + gl.glDisable(gl.GL_SCISSOR_TEST) + # gl.glScissor(0, 0, width, height) + gl.glClearColor(0., 0., 0., 0.) + gl.glClear(gl.GL_COLOR_BUFFER_BIT) + gl.glUniform1i(program.uniforms['texture'], fbo.texture.texUnit) + gl.glEnableVertexAttribArray(program.attributes['position']) + gl.glVertexAttribPointer(program.attributes['position'], + 4, + gl.GL_FLOAT, + gl.GL_FALSE, + 0, + self._position) + fbo.texture.bind() + gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._position)) + gl.glBindTexture(gl.GL_TEXTURE_2D, 0) |