Source code for fsleyes.gl.textures.rendertexturestack

#
# rendertexturestack.py - The RenderTextureStack class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`RenderTextureStack` class, which is used
by the :class:`.SliceCanvas` class to store a collection of off-screen
:class:`.RenderTexture` instances containing rendered slices of
:class:`.GLObject` instances.
"""


from __future__ import division

import logging

import numpy     as np
import OpenGL.GL as gl

import fsleyes.gl.routines as glroutines
import fsl.utils.idle      as idle
from . import                 rendertexture


log = logging.getLogger(__name__)


[docs]class RenderTextureStack(object): """The ``RenderTextureStack`` class creates and maintains a collection of :class:`.RenderTexture` instances, each of which is used to display a single slice of a :class:`.GLObject` along a specific display axis. The purpose of the ``RenderTextureStack`` is to pre-generate 2D slices of a :class:`.GLObject` so that they do not have to be rendered on-demand. Rendering a ``GLObject`` slices from a pre-generated off-screen texture provides better performance than rendering the ``GLObject`` slice in real time. The :class:`.RenderTexture` textures are updated in an idle loop, via the :func:`.idle.idle` function. .. note:: A ``RenderTextureStack`` instance must be manually updated whenever its ``GLObject`` changes, via the :meth:`onGLObjectUpdate` method. ``RenderTextureStack`` instances do not explicitly listen for ``GLObject`` changes themselves, because there is no guarantee that the textures will be refreshed before they need to be drawn. """
[docs] def __init__(self, globj): """Create a ``RenderTextureStack``. An update listener is registered on the ``GLObject``, so that the textures can be refreshed whenever it changes. :arg globj: The :class:`.GLObject` instance. """ self.name = '{}_{}_{}'.format( type(self).__name__, type(globj).__name__, id(self)) self.__globj = globj self.__maxNumTextures = 256 self.__maxWidth = 1024 self.__maxHeight = 1024 self.__defaultNumTextures = 64 self.__defaultWidth = 256 self.__defaultHeight = 256 self.__textureDirty = [] self.__textures = [] self.__lastDrawnTexture = None self.__updateQueue = [] idle.idle(self.__textureUpdateLoop) log.debug('{}.init ({})'.format(type(self).__name__, id(self)))
[docs] def __del__(self): """Prints a log message.""" if log: log.debug('{}.del ({})'.format(type(self).__name__, id(self)))
[docs] def destroy(self): """Must be called when this ``RenderTextureStack`` is no longer needed. Calls the :meth:`__destroyTextures` method. """ self.__destroyTextures()
[docs] def getGLObject(self): """Returns the :class:`.GLObject` associated with this ``RenderTextureStack``. """ return self.__globj
[docs] def draw(self, zpos, xform=None): """Draws the pre-generated :class:`.RenderTexture` which corresponds to the specified Z position. :arg zpos: Position of slice to render. :arg xform: Transformation matrix to apply to rendered slice vertices. """ xax = self.__xax yax = self.__yax texIdx = self.__zposToIndex(zpos) self.__lastDrawnTexture = texIdx if texIdx < 0 or texIdx >= len(self.__textures): return lo, hi = self.__globj.getDisplayBounds() texture = self.__textures[texIdx] if self.__textureDirty[texIdx]: self.__refreshTexture(texture, texIdx) log.debug('Drawing pre-rendered texture ' '[zax {}]: (zpos {}, slice {})'.format( self.__zax, zpos, texIdx)) texture.drawOnBounds( zpos, lo[xax], hi[xax], lo[yax], hi[yax], xax, yax, xform)
[docs] def setAxes(self, xax, yax): """This method must be called when the display orientation of the :class:`.GLObject` changes. It destroys and re-creates all :class:`.RenderTexture` instances. """ zax = 3 - xax - yax self.__xax = xax self.__yax = yax self.__zax = zax res = self.__globj.getDataResolution(xax, yax) if res is not None: numTextures = res[zax] else: numTextures = self.__defaultNumTextures if numTextures > self.__maxNumTextures: numTextures = self.__maxNumTextures self.__destroyTextures() for i in range(numTextures): rt = rendertexture.RenderTexture( '{}_{}'.format(self.name, i), rttype='c') self.__textures.append(rt) self.onGLObjectUpdate()
def __destroyTextures(self): """Destroys all :class:`.RenderTexture` instances. This is performed asynchronously, via the ``idle.idle`` function. """ texes = self.__textures self.__textures = [] for tex in texes: idle.idle(tex.destroy)
[docs] def onGLObjectUpdate(self): """Must be called called when the :class:`.GLObject` display is updated. Re-calculates the display space Z-axis range, and marks all render textures as dirty. """ lo, hi = self.__globj.getDisplayBounds() self.__zmin = lo[self.__zax] self.__zmax = hi[self.__zax] log.debug('GLObject range [zax {}]: {} - {}'.format( self.__zax, self.__zmin, self.__zmax)) self.__refreshAllTextures()
def __refreshAllTextures(self, *a): """Marks all :class:`.RenderTexture` instances as *dirty*, so that they will be refreshed by the :meth:`.__textureUpdateLoop`. """ if self.__lastDrawnTexture is not None: lastIdx = self.__lastDrawnTexture else: lastIdx = len(self.__textures) // 2 aboveIdxs = list(range(lastIdx, len(self.__textures))) belowIdxs = list(range(lastIdx - 1, -1, -1)) idxs = [0] * len(self.__textures) for i in range(len(self.__textures)): if len(aboveIdxs) > 0 and len(belowIdxs) > 0: if i % 2: idxs[i] = belowIdxs.pop(0) else: idxs[i] = aboveIdxs.pop(0) elif len(aboveIdxs) > 0: idxs[i] = aboveIdxs.pop(0) else: idxs[i] = belowIdxs.pop(0) self.__textureDirty = [True] * len(self.__textures) self.__updateQueue = idxs idle.idle(self.__textureUpdateLoop) def __textureUpdateLoop(self): """This method is called via the :func:`.idle.idle` function. It loops through all :class:`.RenderTexture` instances, and refreshes any that have been marked as *dirty*. Each call to this method causes one ``RenderTexture`` to be refreshed. After a ``RenderTexture`` has been refreshed, if there are dirty more ``RenderTexture`` instances, this method re-schedules itself to be called again via :func:`.idle.idle`. """ if len(self.__updateQueue) == 0 or len(self.__textures) == 0: return idx = self.__updateQueue.pop(0) if self.__textureDirty[idx]: tex = self.__textures[idx] log.debug('Refreshing texture slice {} (zax {})'.format( idx, self.__zax)) self.__refreshTexture(tex, idx) if len(self.__updateQueue) > 0: idle.idle(self.__textureUpdateLoop) def __refreshTexture(self, tex, idx): """Refreshes the given :class:`.RenderTexture`. :arg tex: The ``RenderTexture`` to refresh. :arg idx: Index of the ``RenderTexture``. """ globj = self.__globj zpos = self.__indexToZpos(idx) xax = self.__xax yax = self.__yax axes = (self.__xax, self.__yax, self.__zax) if not globj.ready(): return lo, hi = globj.getDisplayBounds() res = globj.getDataResolution(xax, yax) if res is not None: width = res[xax] height = res[yax] else: width = self.__defaultWidth height = self.__defaultHeight if width > self.__maxWidth: width = self.__maxWidth if height > self.__maxHeight: height = self.__maxHeight log.debug('Refreshing render texture for slice {} (zpos {}, ' 'zax {}): {} x {}'.format(idx, zpos, self.__zax, width, height)) tex.shape = width, height oldSize = gl.glGetIntegerv(gl.GL_VIEWPORT) oldProjMat = gl.glGetFloatv( gl.GL_PROJECTION_MATRIX) oldMVMat = gl.glGetFloatv( gl.GL_MODELVIEW_MATRIX) with tex.target(): glroutines.show2D(xax, yax, width, height, lo, hi) glroutines.clear((0, 0, 0, 0)) with glroutines.disabled(gl.GL_BLEND): globj.preDraw() globj.draw2D(zpos, axes) globj.postDraw() gl.glViewport(*oldSize) gl.glMatrixMode(gl.GL_PROJECTION) gl.glLoadMatrixf(oldProjMat) gl.glMatrixMode(gl.GL_MODELVIEW) gl.glLoadMatrixf(oldMVMat) self.__textureDirty[idx] = False def __zposToIndex(self, zpos): """Converts a Z location in the display coordinate system into a ``RenderTexture`` index. """ zmin = self.__zmin zmax = self.__zmax ntexs = len(self.__textures) step = (zmax - zmin) / float(ntexs) # Round to avoid floating # point imprecision. index = np.around((zpos - zmin) / step, 5) # Be a little bit lenient at the boundariese if abs(index) < 0.01: index = 0 elif abs(index - ntexs) < 0.01: index = ntexs - 1 # But make sure that negative indices stay # negative, as otherwise, values between # -0.5 and 0 will be rounded to 0 in the # int(index) call below elif index < 0: index = -1 return int(index) def __indexToZpos(self, index): """Converts a ``RenderTexture`` index into a Z location in the display coordinate system. """ zmin = self.__zmin zmax = self.__zmax ntexs = len(self.__textures) step = (zmax - zmin) / float(ntexs) return index * (zmax - zmin) / ntexs + (zmin + 0.5 * step)