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):
"""Create a ``GLSimpleObject``. """
GLObject.__init__(self)
def destroy( self):
"""Overrides :meth:`GLObject.destroy`. Does nothing. """
pass
......
#!/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
......
#!/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
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