From 50d296e106ab9df90f52e26e8a078614551de0c9 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Tue, 13 Oct 2015 13:48:07 +0100 Subject: [PATCH] Documentation for rendertexture and rendertexturestack modules. --- fsl/fsleyes/gl/globject.py | 1 + fsl/fsleyes/gl/textures/rendertexture.py | 171 +++++++++++-- fsl/fsleyes/gl/textures/rendertexturestack.py | 234 +++++++++++------- 3 files changed, 293 insertions(+), 113 deletions(-) diff --git a/fsl/fsleyes/gl/globject.py b/fsl/fsleyes/gl/globject.py index 210f7a8fb..e3e4a2016 100644 --- a/fsl/fsleyes/gl/globject.py +++ b/fsl/fsleyes/gl/globject.py @@ -290,6 +290,7 @@ class GLSimpleObject(GLObject): """Create a ``GLSimpleObject``. """ GLObject.__init__(self) + def destroy( self): """Overrides :meth:`GLObject.destroy`. Does nothing. """ pass diff --git a/fsl/fsleyes/gl/textures/rendertexture.py b/fsl/fsleyes/gl/textures/rendertexture.py index 11771f57f..1279e7783 100644 --- a/fsl/fsleyes/gl/textures/rendertexture.py +++ b/fsl/fsleyes/gl/textures/rendertexture.py @@ -1,9 +1,18 @@ #!/usr/bin/env python # -# rendertexture.py - +# rendertexture.py - The RenderTexture and GLObjectRenderTexture classes. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""This module provides the :class:`RenderTexture` and +:class:`GLObjectRenderTexture` classes, which are :class:`.Texture2D` +sub-classes intended to be used as targets for off-screen rendering. + +These classes are used by the :class:`.SliceCanvas` and +:class:`.LightBoxCanvas` classes for off-screen rendering. See also the +:class:`.RenderTextureStack`, which uses :class:`RenderTexture` instances. +""" + import logging @@ -19,16 +28,48 @@ log = logging.getLogger(__name__) class RenderTexture(texture.Texture2D): - """A 2D texture and frame buffer, intended to be used as a target for - off-screen rendering of a scene. + """The ``RenderTexture`` class encapsulates a 2D texture, a frame buffer, + and a render buffer, intended to be used as a target for off-screen + rendering. Using a ``RenderTexture`` (``tex`` in the example below) + as the rendering target is easy:: + + # Set the texture size in pixels + tex.setSize(1024, 768) + + # Bind the texture/frame buffer, and configure + # the viewport for orthoghraphic display. + lo = (0.0, 0.0, 0.0) + hi = (1.0, 1.0, 1.0) + tex.bindAsRenderTarget() + tex.setRenderViewport(0, 1, lo, hi) + + # ... + # draw the scene + # ... + + # Unbind the texture/frame buffer, + # and restore the previous viewport. + tex.unbindAsRenderTarget() + tex.restoreViewport() + + + The contents of the ``RenderTexture`` can later be drawn to the screen + via the :meth:`.Texture2D.draw` or :meth:`.Texture2D.drawOnBounds` + methods. """ def __init__(self, name, interp=gl.GL_NEAREST): - """ + """Create a ``RenderTexture``. + + :arg name: A unique name for this ``RenderTexture``. - Note that a current target must have been set for the GL context - before a frameBuffer can be created ... in other words, call - ``context.SetCurrent`` before creating a ``RenderTexture``). + :arg interp: Texture interpolation - either ``GL_NEAREST`` (the + default) or ``GL_LINEAR``. + + .. note:: A rendering target must have been set for the GL context + before a frame buffer can be created ... in other words, + call ``context.SetCurrent`` before creating a + ``RenderTexture``. """ texture.Texture2D.__init__(self, name, interp) @@ -47,7 +88,12 @@ class RenderTexture(texture.Texture2D): def destroy(self): - texture.Texture.destroy(self) + """Must be called when this ``RenderTexture`` is no longer needed. + Destroys the frame buffer and render buffer, and calls + :meth:`.Texture2D.destroy`. + """ + + texture.Texture2D.destroy(self) log.debug('Deleting RB{}/FBO{}'.format( self.__renderBuffer, @@ -57,11 +103,33 @@ class RenderTexture(texture.Texture2D): def setData(self, data): + """Raises a :exc:`NotImplementedError`. The ``RenderTexture`` derives + from the :class:`.Texture2D` class, but is not intended to have its + texture data manually set - see the :class:`.Texture2D` documentation. + """ raise NotImplementedError('Texture data cannot be set for {} ' 'instances'.format(type(self).__name__)) def setRenderViewport(self, xax, yax, lo, hi): + """Configures the GL viewport for a 2D orthographic display. See the + :func:`.routines.show2D` function. + + The existing viewport settings are cached, and can be restored via + the :meth:`restoreViewport` method. + + :arg xax: The display coordinate system axis which corresponds to the + horizontal screen axis. + + :arg yax: The display coordinate system axis which corresponds to the + vertical screen axis. + + :arg lo: A tuple containing the minimum ``(x, y, z)`` display + coordinates. + + :arg hi: A tuple containing the maximum ``(x, y, z)`` display + coordinates. + """ if self.__oldSize is not None or \ self.__oldProjMat is not None or \ @@ -85,6 +153,9 @@ class RenderTexture(texture.Texture2D): def restoreViewport(self): + """Restores the GL viewport settings which were saved via a prior call + to :meth:`setRenderViewport`. + """ if self.__oldSize is None or \ self.__oldProjMat is None or \ @@ -110,6 +181,12 @@ class RenderTexture(texture.Texture2D): def bindAsRenderTarget(self): + """Configures the frame buffer and render buffer of this + ``RenderTexture`` as the targets for rendering. + + The existing farme buffer and render buffer are cached, and can be + restored via the :meth:`unbindAsRenderTarget` method. + """ if self.__oldFrameBuffer is not None or \ self.__oldRenderBuffer is not None: @@ -133,6 +210,9 @@ class RenderTexture(texture.Texture2D): def unbindAsRenderTarget(self): + """Restores the frame buffer and render buffer which were saved via a + prior call to :meth:`bindAsRenderTarget`. + """ if self.__oldFrameBuffer is None or \ self.__oldRenderBuffer is None: @@ -158,6 +238,10 @@ class RenderTexture(texture.Texture2D): def refresh(self): + """Overrides :meth:`.Texture2D.refresh`. Calls the base-class + implementation, and ensures that the frame buffer and render buffer + of this ``RenderTexture`` are configured correctly. + """ texture.Texture2D.refresh(self) width, height = self.getSize() @@ -184,22 +268,50 @@ class RenderTexture(texture.Texture2D): gl.GL_DEPTH_STENCIL_ATTACHMENT, glfbo.GL_RENDERBUFFER_EXT, self.__renderBuffer) - + + self.unbindAsRenderTarget() + self.unbindTexture() + + # Complain if something is not right if glfbo.glCheckFramebufferStatusEXT(glfbo.GL_FRAMEBUFFER_EXT) != \ glfbo.GL_FRAMEBUFFER_COMPLETE_EXT: - self.unbindAsRenderTarget() - self.unbindTexture() raise RuntimeError('An error has occurred while ' 'configuring the frame buffer') - self.unbindAsRenderTarget() - self.unbindTexture() - class GLObjectRenderTexture(RenderTexture): + """The ``GLObjectRenderTexture`` is a :class:`RenderTexture` intended to + be used for rendering :class:`.GLObject` instances off-screen. + + + The advantage of using a ``GLObjectRenderTexture`` over a + :class:`.RenderTexture` is that a ``GLObjectRenderTexture`` will + automatically adjust its size to suit the resolution of the + :class:`.GLObject` - see the :meth:`.GLObject.getDataResolution` method. + + + In order to accomplish this, the :meth:`setAxes` method must be called + whenever the display orientation changes, so that the render texture + size can be re-calculated. + """ def __init__(self, name, globj, xax, yax, maxResolution=1024): - """ + """Create a ``GLObjectRenderTexture``. + + :arg name: A unique name for this ``GLObjectRenderTexture``. + + :arg globj: The :class:`.GLObject` instance which is to be + rendered. + + :arg xax: Index of the display coordinate system axis to be + used as the horizontal render texture axis. + + :arg yax: Index of the display coordinate system axis to be + used as the vertical render texture axis. + + :arg maxResolution: Maximum resolution in pixels, along either the + horizontal or vertical axis, for this + ``GLObjectRenderTexture``. """ self.__globj = globj @@ -214,27 +326,44 @@ class GLObjectRenderTexture(RenderTexture): self.__updateSize() - - def setAxes(self, xax, yax): - self.__xax = xax - self.__yax = yax - self.__updateSize() - def destroy(self): + """Must be called when this ``GLObjectRenderTexture`` is no longer + needed. Removes the update listener from the :class:`.GLObject`, and + calls :meth:`.RenderTexture.destroy`. + """ name = '{}_{}'.format(self.getTextureName(), id(self)) self.__globj.removeUpdateListener(name) RenderTexture.destroy(self) + + def setAxes(self, xax, yax): + """This method must be called when the display orientation of the + :class:`GLObject` changes. It updates the size of this + ``GLObjectRenderTexture`` so that the resolution and aspect ratio + of the ``GLOBject`` are maintained. + """ + self.__xax = xax + self.__yax = yax + self.__updateSize() + def setSize(self, width, height): + """Raises a :exc:`NotImplementedError`. The size of a + ``GLObjectRenderTexture`` is set automatically. + """ raise NotImplementedError( 'Texture size cannot be set for {} instances'.format( type(self).__name__)) def __updateSize(self, *a): + """Updates the size of this ``GLObjectRenderTexture``, basing it + on the resolution returned by the :meth:`.GLObject.getDataResolution` + method. If that method returns ``None``, a default resolution is used. + + """ globj = self.__globj maxRes = self.__maxResolution diff --git a/fsl/fsleyes/gl/textures/rendertexturestack.py b/fsl/fsleyes/gl/textures/rendertexturestack.py index 9b3f98ad7..96cf5f1ee 100644 --- a/fsl/fsleyes/gl/textures/rendertexturestack.py +++ b/fsl/fsleyes/gl/textures/rendertexturestack.py @@ -1,17 +1,20 @@ #!/usr/bin/env python # -# rendertexturelist.py - +# 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. +""" import logging -import numpy as np import OpenGL.GL as gl import fsl.fsleyes.gl.routines as glroutines -import fsl.utils.transform as transform import rendertexture @@ -19,8 +22,30 @@ log = logging.getLogger(__name__) 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, which is + triggered by the ``wx.EVT_IDLE`` event. + """ + + def __init__(self, globj): + """Create a ``RenderTextureStack``. A listener is registered on the + ``wx.EVT_IDLE`` event, so that the :meth:`__textureUpdateLoop` method + is called periodically. 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( @@ -48,80 +73,54 @@ class RenderTextureStack(object): import wx wx.GetApp().Bind(wx.EVT_IDLE, self.__textureUpdateLoop) - - def __refreshAllTextures(self, *a): - - if self.__lastDrawnTexture is not None: - lastIdx = self.__lastDrawnTexture - else: - lastIdx = len(self.__textures) / 2 - - aboveIdxs = range(lastIdx, len(self.__textures)) - belowIdxs = range(lastIdx, 0, -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] = aboveIdxs.pop(0) - else: idxs[i] = belowIdxs.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 - - - def __zposToIndex(self, zpos): - zmin = self.__zmin - zmax = self.__zmax - ntexs = len(self.__textures) - index = ntexs * (zpos - zmin) / (zmax - zmin) - - limit = len(self.__textures) - 1 + + def destroy(self): + """Must be called when this ``RenderTextureStack`` is no longer needed. + Calls the :meth:`__destroyTextures` method. + """ + self.__destroyTextures() - if index > limit and index <= limit + 1: - index = limit - return int(index) + def getGLObject(self): + """Returns the :class:`.GLObject` associated with this + ``RenderTextureStack``. + """ + return self.__globj - def __indexToZpos(self, index): - zmin = self.__zmin - zmax = self.__zmax - ntexs = len(self.__textures) - return index * (zmax - zmin) / ntexs + zmin + 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. - def __textureUpdateLoop(self, ev): - ev.Skip() + :arg xform: Transformation matrix to apply to rendered slice vertices. + """ - if len(self.__updateQueue) == 0 or len(self.__textures) == 0: - return + xax = self.__xax + yax = self.__yax - idx = self.__updateQueue.pop(0) + texIdx = self.__zposToIndex(zpos) + self.__lastDrawnTexture = texIdx - if not self.__textureDirty[idx]: + if texIdx < 0 or texIdx >= len(self.__textures): return - tex = self.__textures[idx] - - log.debug('Refreshing texture slice {} (zax {})'.format( - idx, self.__zax)) - - self.__refreshTexture(tex, idx) + lo, hi = self.__globj.getDisplayBounds() + texture = self.__textures[texIdx] - if len(self.__updateQueue) > 0: - ev.RequestMore() + if self.__textureDirty[texIdx]: + self.__refreshTexture(texture, texIdx) - - def getGLObject(self): - return self.__globj + texture.drawOnBounds( + zpos, lo[xax], hi[xax], lo[yax], hi[yax], xax, yax, xform) 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 @@ -151,19 +150,77 @@ class RenderTextureStack(object): def __destroyTextures(self): + """Destroys all :class:`.RenderTexture` instances. This is performed + asynchronously, via the ``.wx.CallLater`` function. + """ import wx texes = self.__textures self.__textures = [] for tex in texes: wx.CallLater(50, tex.destroy) + + + 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 = range(lastIdx, len(self.__textures)) + belowIdxs = range(lastIdx, 0, -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] = aboveIdxs.pop(0) + else: idxs[i] = belowIdxs.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 + + + def __textureUpdateLoop(self, ev): + """This method is called periodically through the ``wx.EVT_IDLE`` + event. It loops through all :class:`.RenderTexture` instances, and + refreshes any that have been marked as *dirty*. + """ + ev.Skip() + + if len(self.__updateQueue) == 0 or len(self.__textures) == 0: + return + + idx = self.__updateQueue.pop(0) + + if not self.__textureDirty[idx]: + return + + tex = self.__textures[idx] - - def destroy(self): - self.__destroyTextures() + log.debug('Refreshing texture slice {} (zax {})'.format( + idx, self.__zax)) + + self.__refreshTexture(tex, idx) + if len(self.__updateQueue) > 0: + ev.RequestMore() + def __refreshTexture(self, tex, idx): + """Refreshes the given :class:`.RenderTexture`. + + :arg tex: The ``RenderTexture`` to refresh. + :arg idx: Index of the ``RenderTexture``. + """ zpos = self.__indexToZpos(idx) xax = self.__xax @@ -209,35 +266,28 @@ class RenderTextureStack(object): self.__textureDirty[idx] = False - - def draw(self, zpos, xform=None): - - xax = self.__xax - yax = self.__yax - zax = self.__zax - - 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) + 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) + limit = len(self.__textures) - 1 + index = ntexs * (zpos - zmin) / (zmax - zmin) - vertices = np.zeros((6, 3), dtype=np.float32) - vertices[:, zax] = zpos - vertices[0, [xax, yax]] = lo[xax], lo[yax] - vertices[1, [xax, yax]] = lo[xax], hi[yax] - vertices[2, [xax, yax]] = hi[xax], lo[yax] - vertices[3, [xax, yax]] = hi[xax], lo[yax] - vertices[4, [xax, yax]] = lo[xax], hi[yax] - vertices[5, [xax, yax]] = hi[xax], hi[yax] + if index > limit and index <= limit + 1: + index = limit - if xform is not None: - vertices = transform.transform(vertices, xform=xform) + return int(index) - texture.draw(vertices) + + 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) + return index * (zmax - zmin) / ntexs + zmin -- GitLab