#
# program.py - The ARBPShader class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`ARBPShader` class, which encapsulates
an OpenGL shader program written according to the ``ARB_vertex_program``
and ``ARB_fragment_program`` extensions.
"""
from __future__ import division
import re
import logging
import numpy as np
import OpenGL.GL as gl
import OpenGL.raw.GL._types as gltypes
import OpenGL.GL.ARB.fragment_program as arbfp
import OpenGL.GL.ARB.vertex_program as arbvp
import fsl.utils.memoize as memoize
from . import parse
log = logging.getLogger(__name__)
[docs]class ARBPShader(object):
"""The ``ARBPShader`` class encapsulates an OpenGL shader program
written according to the ``ARB_vertex_program`` and
``ARB_fragment_program`` extensions. It parses and compiles vertex
and fragment program code, and provides methods to load/unload
the program, and to set vertex/fragment program parameters and vertex
attributes.
The ``ARBPShader`` class assumes that vertex/fragment program source
has been written to work with the functions defined in the
:mod:`.arbp.parse` module, which allows programs to be written so that
parameter, vertex attribute and texture locations do not have to be hard
coded in the source. Texture locations may be specified in
:meth:`__init__`, and parameter/vertex attribute locations are
automatically assigned by the ``ARBPShader``.
The following methods are available on an ``ARBPShader`` instance:
.. autosummary::
:nosignatures:
load
unload
destroy
recompile
setVertParam
setFragParam
setAtt
setConstant
Typcical usage of an ``ARBPShader`` will look something like the
following::
vertSrc = 'vertex shader source'
fragSrc = 'vertex shader source'
# You must specify the texture unit
# assignments at creation time.
textures = {
'colourMapTexture' : 0,
'dataTexture' : 1
}
program = ARBPShader(vertSrc, fragSrc, textures)
# Load the program, and
# enable program attributes
# (texture coordinates)
program.load()
progra.loadAtts()
# Set some parameters
program.setVertParam('transform', np.eye(4))
program.setFragParam('clipping', [0, 1, 0, 0])
# Create and set vertex attributes
vertices, normals = createVertices()
program.setAtt('normals', normals)
# Draw the scene
gl.glDrawArrays(gl.GL_TRIANGLES, 0, len(vertices))
# Clear the GL state
program.unloadAtts()
program.unload()
# Delete the program when
# it is no longer needed
program.destroy()
.. warning:: The ``ARBPShader`` uses texture coordinates to pass vertex
attributes to the shader programs. Therefore, if you are using
an ``ARBPShader`` you cannot directly use texture coordinates.
See also the :class:`.GLSLShader`, which provides similar functionality for
GLSL shader programs.
"""
[docs] def __init__(self,
vertSrc,
fragSrc,
includePath,
textureMap=None,
constants=None,
clean=True):
"""Create an ``ARBPShader``.
:arg vertSrc: Vertex program source.
:arg fragSrc: Fragment program source.
:arg textureMap: A dictionary of ``{name : int}`` mappings, specifying
the texture unit assignments.
:arg constants: A dictionary of ``{name : values}`` mappings,
specifying any constant parameters required by the
programs. It is assumed that constant parameters are
shared by the vertex and fragment programs.
:arg includePath: Path to a directory which contains any additional
files that may be included in the given source
files.
:arg clean: If ``True`` (the default), the vertex and fragment
program source is 'cleaned' before compilation - all
comments, empty lines, and unncessary spaces are
removed before compilation.
"""
decs = parse.parseARBP(vertSrc, fragSrc)
vParams = decs['vertParam']
fParams = decs['fragParam']
constantDecs = decs['constant']
if constants is None: constants = {}
if len(vParams) > 0: vParams, vLens = zip(*vParams)
else: vParams, vLens = [], []
if len(fParams) > 0: fParams, fLens = zip(*fParams)
else: fParams, fLens = [], []
vLens = {name : length for name, length in zip(vParams, vLens)}
fLens = {name : length for name, length in zip(fParams, fLens)}
self.vertexSource = vertSrc
self.fragmentSource = fragSrc
self.includePath = includePath
self.vertexProgram = None
self.fragmentProgram = None
self.clean = clean
self.includePath = includePath
self.vertParams = vParams
self.vertParamLens = vLens
self.fragParams = fParams
self.fragParamLens = fLens
self.textures = decs['texture']
self.attrs = decs['attr']
self.constants = constantDecs
self.constantVals = dict(constants)
# See the setAtt method for
# information about this dict
self.__attCache = {}
poses = self.__generatePositions(textureMap)
vpPoses, fpPoses, texPoses, attrPoses = poses
self.vertParamPositions = vpPoses
self.fragParamPositions = fpPoses
self.texturePositions = texPoses
self.attrPositions = attrPoses
self.recompile()
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):
"""Deletes all GL resources managed by this ``ARBPShader``. """
if self.vertexProgram is not None:
arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram))
if self.fragmentProgram is not None:
arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram))
self.vertexProgram = None
self.fragmentProgram = None
[docs] def recompile(self):
"""(Re-)generates the vertex and fragment program source code, and
recompiles the programs.
"""
# As we are compiling new
# vertex/fragment programs,
# we need to invalidate any
# cached parameter values.
# Constants are ok.
self.setVertParam.invalidate()
self.setFragParam.invalidate()
vertSrc, fragSrc = parse.fillARBP(self.vertexSource,
self.fragmentSource,
self.vertParamPositions,
self.vertParamLens,
self.fragParamPositions,
self.fragParamLens,
self.constantVals,
self.texturePositions,
self.attrPositions,
self.includePath)
# Compile the new version, but
# only discard the old version
# if compilation succeeds
vp, fp = self.__compile(vertSrc, fragSrc)
self.destroy()
self.vertexProgram = vp
self.fragmentProgram = fp
[docs] def load(self):
"""Loads the shader program. """
gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB)
gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
arbvp.glBindProgramARB(arbvp.GL_VERTEX_PROGRAM_ARB,
self.vertexProgram)
arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB,
self.fragmentProgram)
[docs] def loadAtts(self):
"""Enables texture coordinates for all shader program attributes. """
for attr in self.attrs:
texUnit = self.__getAttrTexUnit(attr)
gl.glClientActiveTexture(texUnit)
gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY)
[docs] def unload(self):
"""Unloads the shader program. """
gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB)
[docs] def unloadAtts(self):
"""Disables texture coordinates on all texture units. """
for attr in self.attrs:
texUnit = self.__getAttrTexUnit(attr)
gl.glClientActiveTexture(texUnit)
gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY)
self.__attCache = {}
@memoize.Instanceify(memoize.skipUnchanged)
def setVertParam(self, name, value):
"""Sets the value of the specified vertex program parameter.
.. note:: It is assumed that the value is either a sequence of length
4 (for vector parameters), or a ``numpy`` array of shape
``(n, 4)`` (for matrix parameters).
.. note:: This method is decorated by the
:func:`.memoize.skipUnchanged` decorator, which returns
``True`` if the value was changed, and ``False`` otherwise.
"""
pos = self.vertParamPositions[name]
value = self.__normaliseParam(value)
nrows = len(value) // 4
log.debug('Setting vertex parameter {} = {}'.format(name, value))
for i in range(nrows):
row = value[i * 4: i * 4 + 4]
arbvp.glProgramLocalParameter4fARB(
arbvp.GL_VERTEX_PROGRAM_ARB, pos + i,
row[0], row[1], row[2], row[3])
@memoize.Instanceify(memoize.skipUnchanged)
def setFragParam(self, name, value):
"""Sets the value of the specified vertex program parameter. See
:meth:`setVertParam` for infomration about possible values.
.. note:: This method is decorated by the
:func:`.memoize.skipUnchanged` decorator, which returns
``True`` if the value was changed, and ``False`` otherwise.
"""
pos = self.fragParamPositions[name]
value = self.__normaliseParam(value)
nrows = len(value) // 4
log.debug('Setting fragment parameter {} = {}'.format(name, value))
for i in range(nrows):
row = value[i * 4: i * 4 + 4]
arbfp.glProgramLocalParameter4fARB(
arbfp.GL_FRAGMENT_PROGRAM_ARB, pos + i,
row[0], row[1], row[2], row[3])
@memoize.Instanceify(memoize.skipUnchanged)
def setConstant(self, name, value):
"""Updates the value of a constant parameter used by the program.
The :meth:`recompile` method must be called after changing a constant
value.
"""
if name not in self.constants:
raise ValueError('Unknown constant: {}'.format(name))
log.debug('Setting vertex constant {} = {}'.format(name, value))
self.constantVals[name] = value
[docs] def setAtt(self, name, value):
"""Sets the value of the specified vertex attribute. Each vertex
attribute is mapped to a texture coordinate. It is assumed that
the given value is a ``numpy`` array of shape ``(n, l)``, where
``n`` is the number of vertices being drawn, and ``l`` is the
number of components in each vertex attribute coordinate.
"""
texUnit = self.__getAttrTexUnit(name)
size = value.shape[1]
value = np.array(value, dtype=np.float32, copy=False)
log.debug('Setting vertex attribute {} [{}] = [{} * {}]'.format(
name, texUnit, value.shape[0], size))
# We must save a ref to the value so
# that it doesn't get GC'd by python
# before actually being used by GL.
# This took me an entire day to
# figure out. The cache gets cleared
# on every call to unloadAtts.
value = value.ravel('C')
self.__attCache[name] = value
gl.glClientActiveTexture(texUnit)
gl.glTexCoordPointer(size, gl.GL_FLOAT, 0, value)
def __normaliseParam(self, value):
"""Used by :meth:`setVertParam` and :meth:`setFragParam`. Ensures that
all vertex/fragment program parameters are vectors of length 4, or
matrices of size ``(n, 4)``.
"""
# scalar
if np.isscalar(value):
value = [value]
value = np.array(value, copy=False)
# vector
if len(value.shape) == 1:
# if < 4 values, pad it to 4. If > 4
# values, an error will be raised below
if value.shape[0] < 4:
value = list(value) + [0] * (4 - len(value))
value = np.array(value, dtype=np.float32, copy=False)
if value.size < 4 or value.size % 4 != 0:
raise ValueError('Invalid arbp parameter: {}'.format(value))
return value.ravel('C')
def __getAttrTexUnit(self, attr):
"""Returns the texture unit identifier which corresponds to the named
vertex attribute.
"""
pos = self.attrPositions[attr]
texUnit = 'GL_TEXTURE{}'.format(pos)
texUnit = getattr(gl, texUnit)
return texUnit
def __generatePositions(self, textureMap=None):
"""Called by :meth:`__init__`. Generates positions for vertex/fragment
program parameters and vertex attributes.
The lengths of each vertex/fragment parameter are known (see
:mod:`.arbp.parse`), so these parameters are set up to be sequentially
stored in the program parameter memory.
Vertex attributes are passed to the vertex program as texture
coordinates.
If texture units were not specified in ``__init__``, texture units are
also automatically assigned to each texture used in the fragment
program.
"""
vpPoses = {}
fpPoses = {}
texPoses = {}
attrPoses = {}
# Vertex parameters
pos = 0
for name in self.vertParams:
vpPoses[name] = pos
pos += self.vertParamLens[name]
# Fragment parameters
pos = 0
for name in self.fragParams:
fpPoses[name] = pos
pos += self.fragParamLens[name]
# Vertex attributes
for i, name in enumerate(self.attrs):
attrPoses[name] = i
# Texture positions. If the caller did
# not provide a texture map in __init__,
# we'll generate some positions.
if textureMap is None:
names = self.textures
poses = list(range(len(names)))
texPoses = {n : p for n, p in zip(names, poses)}
else:
texPoses = dict(textureMap)
return vpPoses, fpPoses, texPoses, attrPoses
def __cleanSource(self, src):
"""Strips out comments and blank lines from the given string, unless
``clean is False`` (as passed into :meth:`__init__`).
"""
if not self.clean:
return src
# strip out comments and blank lines
lines = src.split('\n')
lines = [l.strip() for l in lines]
lines = [l for l in lines if l != '']
lines = [l for l in lines if l[0] != '#']
src = '\n'.join(lines)
# Squeeze duplicate spaces
src = re.sub(' +', ' ', src)
return src
def __compile(self, vertSrc, fragSrc):
"""Called by :meth:`__init__`. Compiles the vertex and fragment
programs and returns references to the compiled programs.
"""
gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB)
gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
# Clear out unnecessary stuff from
# the source, and make sure it is
# plain ASCII - not unicode.
vertSrc = self.__cleanSource(vertSrc)
fragSrc = self.__cleanSource(fragSrc)
vertSrc = vertSrc.encode('ascii')
fragSrc = fragSrc.encode('ascii')
fragProg = arbfp.glGenProgramsARB(1)
vertProg = arbvp.glGenProgramsARB(1)
# vertex program
try:
arbvp.glBindProgramARB(arbvp.GL_VERTEX_PROGRAM_ARB, vertProg)
arbvp.glProgramStringARB(arbvp.GL_VERTEX_PROGRAM_ARB,
arbvp.GL_PROGRAM_FORMAT_ASCII_ARB,
len(vertSrc),
vertSrc)
except Exception:
position = gl.glGetIntegerv(arbvp.GL_PROGRAM_ERROR_POSITION_ARB)
message = gl.glGetString( arbvp.GL_PROGRAM_ERROR_STRING_ARB)
message = message.decode('ascii')
raise RuntimeError('Error compiling vertex program ({}): '
'{}\n{}'.format(position, message, vertSrc))
# fragment program
try:
arbfp.glBindProgramARB(arbfp.GL_FRAGMENT_PROGRAM_ARB,
fragProg)
arbfp.glProgramStringARB(arbfp.GL_FRAGMENT_PROGRAM_ARB,
arbfp.GL_PROGRAM_FORMAT_ASCII_ARB,
len(fragSrc),
fragSrc)
except Exception:
position = gl.glGetIntegerv(arbfp.GL_PROGRAM_ERROR_POSITION_ARB)
message = gl.glGetString( arbfp.GL_PROGRAM_ERROR_STRING_ARB)
message = message.decode('ascii')
raise RuntimeError('Error compiling fragment program ({}): '
'{}\n{}'.format(position, message, fragSrc))
gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB)
gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB)
return vertProg, fragProg