Skip to content
Snippets Groups Projects
Commit 50d296e1 authored by Paul McCarthy's avatar Paul McCarthy
Browse files

Documentation for rendertexture and rendertexturestack modules.

parent 36eb4b9d
No related branches found
No related tags found
No related merge requests found
...@@ -290,6 +290,7 @@ class GLSimpleObject(GLObject): ...@@ -290,6 +290,7 @@ class GLSimpleObject(GLObject):
"""Create a ``GLSimpleObject``. """ """Create a ``GLSimpleObject``. """
GLObject.__init__(self) GLObject.__init__(self)
def destroy( self): def destroy( self):
"""Overrides :meth:`GLObject.destroy`. Does nothing. """ """Overrides :meth:`GLObject.destroy`. Does nothing. """
pass pass
......
#!/usr/bin/env python #!/usr/bin/env python
# #
# rendertexture.py - # rendertexture.py - The RenderTexture and GLObjectRenderTexture classes.
# #
# Author: Paul McCarthy <pauldmccarthy@gmail.com> # 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 import logging
...@@ -19,16 +28,48 @@ log = logging.getLogger(__name__) ...@@ -19,16 +28,48 @@ log = logging.getLogger(__name__)
class RenderTexture(texture.Texture2D): class RenderTexture(texture.Texture2D):
"""A 2D texture and frame buffer, intended to be used as a target for """The ``RenderTexture`` class encapsulates a 2D texture, a frame buffer,
off-screen rendering of a scene. 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): 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 :arg interp: Texture interpolation - either ``GL_NEAREST`` (the
before a frameBuffer can be created ... in other words, call default) or ``GL_LINEAR``.
``context.SetCurrent`` before creating a ``RenderTexture``).
.. 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) texture.Texture2D.__init__(self, name, interp)
...@@ -47,7 +88,12 @@ class RenderTexture(texture.Texture2D): ...@@ -47,7 +88,12 @@ class RenderTexture(texture.Texture2D):
def destroy(self): 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( log.debug('Deleting RB{}/FBO{}'.format(
self.__renderBuffer, self.__renderBuffer,
...@@ -57,11 +103,33 @@ class RenderTexture(texture.Texture2D): ...@@ -57,11 +103,33 @@ class RenderTexture(texture.Texture2D):
def setData(self, data): 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 {} ' raise NotImplementedError('Texture data cannot be set for {} '
'instances'.format(type(self).__name__)) 'instances'.format(type(self).__name__))
def setRenderViewport(self, xax, yax, lo, hi): 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 \ if self.__oldSize is not None or \
self.__oldProjMat is not None or \ self.__oldProjMat is not None or \
...@@ -85,6 +153,9 @@ class RenderTexture(texture.Texture2D): ...@@ -85,6 +153,9 @@ class RenderTexture(texture.Texture2D):
def restoreViewport(self): def restoreViewport(self):
"""Restores the GL viewport settings which were saved via a prior call
to :meth:`setRenderViewport`.
"""
if self.__oldSize is None or \ if self.__oldSize is None or \
self.__oldProjMat is None or \ self.__oldProjMat is None or \
...@@ -110,6 +181,12 @@ class RenderTexture(texture.Texture2D): ...@@ -110,6 +181,12 @@ class RenderTexture(texture.Texture2D):
def bindAsRenderTarget(self): 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 \ if self.__oldFrameBuffer is not None or \
self.__oldRenderBuffer is not None: self.__oldRenderBuffer is not None:
...@@ -133,6 +210,9 @@ class RenderTexture(texture.Texture2D): ...@@ -133,6 +210,9 @@ class RenderTexture(texture.Texture2D):
def unbindAsRenderTarget(self): 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 \ if self.__oldFrameBuffer is None or \
self.__oldRenderBuffer is None: self.__oldRenderBuffer is None:
...@@ -158,6 +238,10 @@ class RenderTexture(texture.Texture2D): ...@@ -158,6 +238,10 @@ class RenderTexture(texture.Texture2D):
def refresh(self): 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) texture.Texture2D.refresh(self)
width, height = self.getSize() width, height = self.getSize()
...@@ -184,22 +268,50 @@ class RenderTexture(texture.Texture2D): ...@@ -184,22 +268,50 @@ class RenderTexture(texture.Texture2D):
gl.GL_DEPTH_STENCIL_ATTACHMENT, gl.GL_DEPTH_STENCIL_ATTACHMENT,
glfbo.GL_RENDERBUFFER_EXT, glfbo.GL_RENDERBUFFER_EXT,
self.__renderBuffer) self.__renderBuffer)
self.unbindAsRenderTarget()
self.unbindTexture()
# Complain if something is not right
if glfbo.glCheckFramebufferStatusEXT(glfbo.GL_FRAMEBUFFER_EXT) != \ if glfbo.glCheckFramebufferStatusEXT(glfbo.GL_FRAMEBUFFER_EXT) != \
glfbo.GL_FRAMEBUFFER_COMPLETE_EXT: glfbo.GL_FRAMEBUFFER_COMPLETE_EXT:
self.unbindAsRenderTarget()
self.unbindTexture()
raise RuntimeError('An error has occurred while ' raise RuntimeError('An error has occurred while '
'configuring the frame buffer') 'configuring the frame buffer')
self.unbindAsRenderTarget()
self.unbindTexture()
class GLObjectRenderTexture(RenderTexture): 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): 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 self.__globj = globj
...@@ -214,27 +326,44 @@ class GLObjectRenderTexture(RenderTexture): ...@@ -214,27 +326,44 @@ class GLObjectRenderTexture(RenderTexture):
self.__updateSize() self.__updateSize()
def setAxes(self, xax, yax):
self.__xax = xax
self.__yax = yax
self.__updateSize()
def destroy(self): 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)) name = '{}_{}'.format(self.getTextureName(), id(self))
self.__globj.removeUpdateListener(name) self.__globj.removeUpdateListener(name)
RenderTexture.destroy(self) 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): def setSize(self, width, height):
"""Raises a :exc:`NotImplementedError`. The size of a
``GLObjectRenderTexture`` is set automatically.
"""
raise NotImplementedError( raise NotImplementedError(
'Texture size cannot be set for {} instances'.format( 'Texture size cannot be set for {} instances'.format(
type(self).__name__)) type(self).__name__))
def __updateSize(self, *a): 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 globj = self.__globj
maxRes = self.__maxResolution maxRes = self.__maxResolution
......
#!/usr/bin/env python #!/usr/bin/env python
# #
# rendertexturelist.py - # rendertexturestack.py - The RenderTextureStack class.
# #
# Author: Paul McCarthy <pauldmccarthy@gmail.com> # 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 logging
import numpy as np
import OpenGL.GL as gl import OpenGL.GL as gl
import fsl.fsleyes.gl.routines as glroutines import fsl.fsleyes.gl.routines as glroutines
import fsl.utils.transform as transform
import rendertexture import rendertexture
...@@ -19,8 +22,30 @@ log = logging.getLogger(__name__) ...@@ -19,8 +22,30 @@ log = logging.getLogger(__name__)
class RenderTextureStack(object): 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): 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( self.name = '{}_{}_{}'.format(
...@@ -48,80 +73,54 @@ class RenderTextureStack(object): ...@@ -48,80 +73,54 @@ class RenderTextureStack(object):
import wx import wx
wx.GetApp().Bind(wx.EVT_IDLE, self.__textureUpdateLoop) wx.GetApp().Bind(wx.EVT_IDLE, self.__textureUpdateLoop)
def __refreshAllTextures(self, *a): def destroy(self):
"""Must be called when this ``RenderTextureStack`` is no longer needed.
if self.__lastDrawnTexture is not None: Calls the :meth:`__destroyTextures` method.
lastIdx = self.__lastDrawnTexture """
else: self.__destroyTextures()
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
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): def draw(self, zpos, xform=None):
zmin = self.__zmin """Draws the pre-generated :class:`.RenderTexture` which corresponds
zmax = self.__zmax to the specified Z position.
ntexs = len(self.__textures)
return index * (zmax - zmin) / ntexs + zmin
:arg zpos: Position of slice to render.
def __textureUpdateLoop(self, ev): :arg xform: Transformation matrix to apply to rendered slice vertices.
ev.Skip() """
if len(self.__updateQueue) == 0 or len(self.__textures) == 0: xax = self.__xax
return 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 return
tex = self.__textures[idx] lo, hi = self.__globj.getDisplayBounds()
texture = self.__textures[texIdx]
log.debug('Refreshing texture slice {} (zax {})'.format(
idx, self.__zax))
self.__refreshTexture(tex, idx)
if len(self.__updateQueue) > 0: if self.__textureDirty[texIdx]:
ev.RequestMore() self.__refreshTexture(texture, texIdx)
texture.drawOnBounds(
def getGLObject(self): zpos, lo[xax], hi[xax], lo[yax], hi[yax], xax, yax, xform)
return self.__globj
def setAxes(self, xax, yax): 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 zax = 3 - xax - yax
self.__xax = xax self.__xax = xax
...@@ -151,19 +150,77 @@ class RenderTextureStack(object): ...@@ -151,19 +150,77 @@ class RenderTextureStack(object):
def __destroyTextures(self): def __destroyTextures(self):
"""Destroys all :class:`.RenderTexture` instances. This is performed
asynchronously, via the ``.wx.CallLater`` function.
"""
import wx import wx
texes = self.__textures texes = self.__textures
self.__textures = [] self.__textures = []
for tex in texes: for tex in texes:
wx.CallLater(50, tex.destroy) 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]
log.debug('Refreshing texture slice {} (zax {})'.format(
def destroy(self): idx, self.__zax))
self.__destroyTextures()
self.__refreshTexture(tex, idx)
if len(self.__updateQueue) > 0:
ev.RequestMore()
def __refreshTexture(self, tex, idx): 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) zpos = self.__indexToZpos(idx)
xax = self.__xax xax = self.__xax
...@@ -209,35 +266,28 @@ class RenderTextureStack(object): ...@@ -209,35 +266,28 @@ class RenderTextureStack(object):
self.__textureDirty[idx] = False 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]: def __zposToIndex(self, zpos):
self.__refreshTexture(texture, texIdx) """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) if index > limit and index <= limit + 1:
vertices[:, zax] = zpos index = limit
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 xform is not None: return int(index)
vertices = transform.transform(vertices, xform=xform)
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment