From 3577668419d721da0ad5767a3a0b151bcd02fc60 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Mon, 4 Jan 2016 14:50:14 +0000 Subject: [PATCH] Documentation for shaders package. --- fsl/fsleyes/gl/shaders/__init__.py | 54 +++- fsl/fsleyes/gl/shaders/arbp/parse.py | 375 +++++++++++++++++++++++-- fsl/fsleyes/gl/shaders/arbp/program.py | 139 ++++++++- fsl/fsleyes/gl/shaders/glsl/program.py | 4 +- 4 files changed, 533 insertions(+), 39 deletions(-) diff --git a/fsl/fsleyes/gl/shaders/__init__.py b/fsl/fsleyes/gl/shaders/__init__.py index 47dd75c03..8b7bbe0c4 100644 --- a/fsl/fsleyes/gl/shaders/__init__.py +++ b/fsl/fsleyes/gl/shaders/__init__.py @@ -1,11 +1,41 @@ #!/usr/bin/env python # -# __init__.py - +# __init__.py - Funtions for managing OpenGL shader programs. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""The ``shaders`` package contains classes and functions for parsing, +compiling, and managing OpenGL shader programs. Two types of shader +program are supported: + + + - GLSL 1.20 vertex and fragment shaders. + + - ``ARB_vertex_program`` and ``ARB_fragment_program`` shader programs. + + +The :mod:`.glsl` and :mod:`.arbp` packages respectively define the +:class:`.GLSLShader` and :class:`.ARBPShader` classes, which may be +used to manage shader programs of the corresponding type. + + +Some package-level functions are defined here, for finding and loading +shader source code: + + + - :func:`getVertexShader`: Locate and load the source code for a specific + vertex shader. + + - :func:`getFragmentShader`: Locate and load the source code for a specific + fragment shader. + + +Each of these functions locate shader source files, load the source code, and +run them through the :func:`preprocess` function, which performs some simple +preprocessing on the source. This applies to both GLSL and ARB assembly +shader programs. +""" -import logging import os.path as op @@ -15,36 +45,34 @@ import glsl.program as glslprogram import arbp.program as arbpprogram -log = logging.getLogger(__name__) - - GLSLShader = glslprogram.GLSLShader ARBPShader = arbpprogram.ARBPShader def getVertexShader(prefix): - """Returns the vertex shader source for the given GL object.""" + """Returns the vertex shader source for the given GL type (e.g. + 'glvolume'). + """ return _getShader(prefix, 'vert') def getFragmentShader(prefix): - """Returns the fragment shader source for the given GL object.""" + """Returns the fragment shader source for the given GL type.""" return _getShader(prefix, 'frag') def _getShader(prefix, shaderType): - """Returns the shader source for the given GL object and the given + """Returns the shader source for the given GL type and the given shader type ('vert' or 'frag'). """ fname = _getFileName(prefix, shaderType) with open(fname, 'rt') as f: src = f.read() - return _preprocess(src) + return preprocess(src) def _getFileName(prefix, shaderType): - """Returns the file name of the shader program for the given GL object - and shader type. The ``globj`` parameter may alternately be a string, - in which case it is used as the prefix for the shader program file name. + """Returns the file name of the shader program for the given GL type + and shader type. """ if fslgl.GL_VERSION == '2.1': @@ -61,7 +89,7 @@ def _getFileName(prefix, shaderType): prefix, shaderType, suffix)) -def _preprocess(src): +def preprocess(src): """'Preprocess' the given shader source. This amounts to searching for lines containing '#pragma include filename', diff --git a/fsl/fsleyes/gl/shaders/arbp/parse.py b/fsl/fsleyes/gl/shaders/arbp/parse.py index 5281ca72f..3228117b3 100644 --- a/fsl/fsleyes/gl/shaders/arbp/parse.py +++ b/fsl/fsleyes/gl/shaders/arbp/parse.py @@ -1,39 +1,334 @@ #!/usr/bin/env python # -# parse.py - +# parse.py - Very simple parser for ARB assembly shader programs. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""This module provides functions for use with OpenGL ``ARB_vertex_program`` +and ``ARB_fragment_program`` assembly source code. It defines a simple +templating system, allowing ARB assembly programs to be written in such a way +that input parameters, vertex attributes, texture locations, and texture +coordinates and do not have to be hard coded in the source. -import re +.. note:: This module is used by the :class:`.ARBPShader` class - if you use + the :class:`.ARBShader` class, you will not need to use this module + at all. -import jinja2 as j2 -import jinja2.meta as j2meta +Instead, place holder tokens can be used in the source code. These tokens may +be parsed (using ``jinja2``) by the :func:`parseARBP` function. Values can +then be assigned to the place holders using the :func:`fillARBP` function. -# -# {{ param_paramName }} -> program.local[X] -# -# Or, if 'paramName' has length > 1 -# -# {{ param4_paramName }} -> { program.local[x], program.local[x + 1], ... } -# {{ attr_attName }} -> vertex.texcoord[N] -# -# {{ texture_texName }} -> texture[N] +An example +---------- +As an example, consider the following vertex program, for drawing a slice +from a 3D image texture:: -# Maybe this too? In vertex program: -# {{ varying_outputName }} -> result.texcoord[N] -# -# In fragment program: -# {{ varying_outputName }} -> fragment.texcoord[N] + + !!ARBvp1.0 + + PARAM imageShape = program.local[0]; + + # Transform the vertex position into display coordinates + DP4 result.position.x, state.matrix.mvp.row[0], vertex.position; + DP4 result.position.y, state.matrix.mvp.row[1], vertex.position; + DP4 result.position.z, state.matrix.mvp.row[2], vertex.position; + DP4 result.position.w, state.matrix.mvp.row[3], vertex.position; + + # Transform the texture coordinates (which are + # between 0 and 1) into voxel coordinates (which + # are within the image voxel dimensions). + MOV voxCoord, vertex.texcoord[0]; + MUL voxCoord, voxCoord, imageShape; + + # Pass the texture coordinates and + # corresponding voxel coordinates + # through to the fragment program. + MOV result.texcoord[0], vertex.texcoord[0]; + MOV result.texcoord[1], voxCoord; + + END + + +And the corresponding fragment program, which looks up the voxel value +and colours the fragment accordingly: + + + !!ARBfp1.0 + + TEMP voxValue; + + # A transformation matrix (encoding a linear + # offset/scale) which transforms a voxel value + # from the image texture data range to the + # colour map texture input coordinate range. + PARAM voxValXform[4] = { program.local[0], + program.local[1], + program.local[2], + program.local[3] }; + + # Get the voxel value + TEX voxValue.x, fragment.texcoord[0], texture[0], 3D; + + # Transform the voxel value + MAD voxValue, voxValue, voxValXform[0].x, voxValXform[3].x; + + # Get the colour that corresponds to the voxel value + TEX result.color, voxValue.x, texture[1], 1D; + + +This program requires: + + - The image shape to be specified as a program parameter at index 0. + + - Image texture coordinates to be passed as coordinates on texture unit 0. + + - Both the vertex and fragment programs to know which texture units the + texture and voxel coordinates are passed through on. + + - The image texture to be bound to texture unit 0. + + - The colour map texture to be bound to texture unit 1. + + +By using this module, all of these requirements can be removed by re-writing +the vertex program as follows:: + + + !!ARBvp1.0 + + PARAM imageShape = {{ param_imageShape }}; + + TEMP voxCoord; + + # Transform the vertex position into display coordinates + DP4 result.position.x, state.matrix.mvp.row[0], vertex.position; + DP4 result.position.y, state.matrix.mvp.row[1], vertex.position; + DP4 result.position.z, state.matrix.mvp.row[2], vertex.position; + DP4 result.position.w, state.matrix.mvp.row[3], vertex.position; + + # Transform the texture coordinates (which are + # between 0 and 1) into voxel coordinates (which + # are within the image voxel dimensions). + MOV voxCoord, {{ attr_texCoord }}; + MUL voxCoord, voxCoord, imageShape; + + # Pass the texture coordinates and + # corresponding voxel coordinates + # through to the fragment program. + MOV {{ varying_texCoord }}, {{ attr_texCoord }}; + MOV {{ varying_voxCoord }}, voxCoord; + + END + + +And the fragment program:: + + + !!ARBfp1.0 + + TEMP voxValue; + + # A transformation matrix (encoding a linear + # offset/scale) which transforms a voxel value + # from the image texture data range to the + # colour map texture input coordinate range. + PARAM voxValXform[4] = {{ param4_voxValXform }}; + + # Get the voxel value + TEX voxValue.x, {{ varying_texCoord }}, {{ texture_imageTexture }}, 3D; + + # Transform the voxel value + MAD voxValue, voxValue, voxValXform[0].x, voxValXform[3].x; + + # Get the colour that corresponds to the voxel value + TEX result.color, voxValue.x, {{ texture_colourMapTexture }}, 1D; + + +The :func:`parseARBP` function parses the source code and returns information +about all declared items. The :func:`fillARBP` function can then be used to +assign explicit values to each of the items:: + + + vertSrc = '!!ARBvp1.0 vertex shader source' + fragSrc = '!!ARBfp1.0 fragment shader source' + + # Get information about all parameters, + # attributes, textures, and varyings. + items = parse.parseARBP(vertSrc, fragSrc) + + # ... + # You have to calculate positions for + # parameters, attributes and textures. + # Positions for varying items are + # automatically calculated for you. + # ... + vertParams = {'imageShape' : 0} + vertParamLens = {'imageShape' : 1} + fragParams = {'voxValXform' : 0} + fragParamLens = {'voxValXform' : 4} + textures = {'imageTexture' : 0, + 'colourMapTexture' : 1} + attrs = {'texCoord' : 0} + + # Fill in the template + vertSrc, fragSrc = parse.fillARBP(vertSrc, + fragSrc, + vertParams, + vertParamLens, + fragParams, + fragParamLens, + textures, + attrs) + + # Now you can compile the source + # code and run your program! + + +Template tokens +--------------- + + +The following items may be specified as template tokens. As depicted in +the example above, a token is specified in the following manner:: + + {{ tokenPrefix_itemName }} + + +Prefixes for each item type are as follows: + + + ===================== ============ + Item Token prefix + ===================== ============ + *Parameters*: ``param`` + *Vertex attributes* ``attr`` + *Textures* ``texture`` + *Varying attributes* ``varying`` + ===================== ============ + + +Parameters +========== + +*Parameters* are constant values which are passed to every instantiation of a +shader program - they are equivalent to ``uniform`` values in a GLSL program. +In a normal ARB assembly program, parameters are accessed as follows:: + + PARAM imageShape = program.local[0]; + + +When using this module, you may instead access parameters in this way:: + + PARAM imageShape = {{ param_imageShape }}; + + +Parameters with a length greater than 1 (e.g. matrix parameters) are +traditionally accessed in this way:: + + PARAM xform[4] = { program.local[0], + program.local[1], + program.local[2], + program.local[3] }; + + +When using this module, you may access matrix parameters in this way:: + + PARAM xform[4] = {{ param4_xform }}; + + +Vertex attributes +================= + + +*Vertex attributes* are values which are associated with every rendered +vertex. They are equivalent to ``attribute`` values in a GLSL program. +In a normal ARB assembly program, one would typically pass vertex +attributes as texture coordinates bound to a specified texture unit:: + + PARAM texCoord = vertex.texcoord[0]; + + +When using this module, you may access vertex attributes as follows:: + + PARAM texCoord = {{ attr_texCoord }}; + + +Textures +======== + +In a typical ARB assembly program, the texture unit to which each texture is +bound must be hard coded:: + + TEX voxelValue, texCoord, texture[0], 3D; + + +This can be avoided by using texture tokens:: + + TEX voxelValue, texCoord, {{ texture_imageTexture }}, 3D; + + +Varying attributes +================== + + +Varying attributes are attributes which are generated in the vertex program, +and passed through to the fragment program. They are equivalent to ``varying`` +values in a GLSXL program. In an ARB assembly program, they are typically +passed and accessed as texture coordinates:: + + !!ARBvp1.0 + # In the vertex program, we pass varying + # attribute through as texture coordinates: + MOV result.texcoord[0], texCoord; + MOV result.texcoord[1], voxCoord; + # ... + + + !!ARBfp1.0 + # ... + # In the fragment program, we access varying + # attrbutes as texture coordinates + TEMP texCoord; + TEMP voxCoord; + MOV texCoord, fragment.texcoord[0]; + MOV voxCoord, fragment.texcoord[1]; + # ... + + +This can be avoided by using the :func:`fillARBP` function, which will +automatically assign texture coordinate positions to each varying attribute. +The assembly code can thus be re-written as follows:: + + !!ARBvp1.0 + # ... + MOV {{ varying_texCoord }}, texCoord; + MOV {{ varying_voxCoord }}, voxCoord; + # ... + + + !!ARBfp1.0 + # ... + TEMP texCoord; + TEMP voxCoord; + MOV texCoord, {{ varying_texCoord }}; + MOV voxCoord, {{ varying_voxCoord }}; + # ... +""" + + +import re + +import jinja2 as j2 +import jinja2.meta as j2meta def parseARBP(vertSrc, fragSrc): """Parses the given ``ARB_vertex_program`` and ``ARB_fragment_program`` - code, and returns information about all declared variables. + code, and returns information about all declared variables. """ vParams, vTextures, vAttrs, vVaryings = _findDeclaredVariables(vertSrc) @@ -58,6 +353,36 @@ def fillARBP(vertSrc, fragParamLens, textures, attrs): + """Fills in the given ARB assembly code, replacing all template tokens + with the values specified by the various arguments. + + :arg vertSrc: Vertex program source. + + :arg fragSrc: Fragment program source. + + :arg vertParams: Dictionary of ``{name : position}`` mappings, + specifying the position indices of all vertex + program parameters. + + :arg vertParamLens: Dictionary ``{name : length}`` mappings, + specifying the lengths of all vertex program + parameters. + + :arg fragParams: Dictionary of ``{name : position}`` mappings, + specifying the position indices of all fragment + program parameters. + + :arg fragParamLens: Dictionary ``{name : length}`` mappings, + specifying the lengths of all fragment program + parameters. + + :arg textures: Dictionary of `{name : textureUnit}`` mappings, + specifying the texture unit to use for each texture. + + :arg attrs: Dictionary of `{name : textureUnit}`` mappings, + specifying the texture unit to use for each vertex + attribute. + """ vertVars = _findDeclaredVariables(vertSrc) fragVars = _findDeclaredVariables(fragSrc) @@ -113,6 +438,9 @@ def fillARBP(vertSrc, def _findDeclaredVariables(source): + """Parses the given ARB assembly program source, and returns information + about all template tokens defined within. + """ env = j2.Environment() ast = env.parse(source) @@ -157,6 +485,9 @@ def _checkVariableValidity(vertVars, fragParamMap, textureMap, attrMap): + """Checks the information about a vertex/fragment program, and raises + an error if it looks like something is wrong. + """ vParams, vTextures, vAttrs, vVaryings = vertVars fParams, fTextures, fAttrs, fVaryings = fragVars @@ -187,6 +518,7 @@ def _checkVariableValidity(vertVars, def _makeVaryingMap(vertVaryings, fragVaryings): + """Generates texture unit indices for all varying attributes. """ indices = range(len(vertVaryings)) varyingMap = dict(zip(vertVaryings, indices)) @@ -194,6 +526,8 @@ def _makeVaryingMap(vertVaryings, fragVaryings): def _param(number, length): + """Generates ARB assembly for the named vertex/fragment program parameter. + """ if length == 1: return 'program.local[{}]'.format(number) @@ -205,13 +539,16 @@ def _param(number, length): def _texture(number): + """Generates ARB assembly for a texture.""" return 'texture[{}]'.format(number) def _attr(number): + """Generates ARB assembly for a vertex attribute. """ return 'vertex.texcoord[{}]'.format(number) def _varying(number, vert): + """Generates ARB assembly for a varying attribute. """ if vert: return 'result.texcoord[{}]' .format(number) else: return 'fragment.texcoord[{}]'.format(number) diff --git a/fsl/fsleyes/gl/shaders/arbp/program.py b/fsl/fsleyes/gl/shaders/arbp/program.py index 6d20a3abb..c4ebc3652 100644 --- a/fsl/fsleyes/gl/shaders/arbp/program.py +++ b/fsl/fsleyes/gl/shaders/arbp/program.py @@ -1,9 +1,13 @@ #!/usr/bin/env python # -# program.py - +# 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. +""" import logging @@ -21,8 +25,92 @@ log = logging.getLogger(__name__) 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 + delete + setVertParam + setFragParam + setAttr + + 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 = GLSLShader(vertSrc, fragSrc, textures) + + # Load the program + program.load() + + # 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.setAttr('normals', normals) + + # Draw the scene + gl.glDrawArrays(gl.GL_TRIANGLES, 0, len(vertices)) + + # Clear the GL state + program.unload() + + # Delete the program when + # it is no longer needed + program.delete() + + + .. 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. + """ def __init__(self, vertSrc, fragSrc, textureMap=None): + """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. + """ decs = parse.parseARBP(vertSrc, fragSrc) @@ -65,14 +153,23 @@ class ARBPShader(object): self.vertexProgram = vp self.fragmentProgram = fp + + log.memory('{}.init({})'.format(type(self).__name__, id(self))) + + + def __del__(self): + """Prints a log message. """ + log.memory('{}.del({})'.format(type(self).__name__, id(self))) def delete(self): + """Deletes all GL resources managed by this ``ARBPShader``. """ arbvp.glDeleteProgramsARB(1, gltypes.GLuint(self.vertexProgram)) arbfp.glDeleteProgramsARB(1, gltypes.GLuint(self.fragmentProgram)) def load(self): + """Loads the shader program. """ gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB) @@ -89,6 +186,7 @@ class ARBPShader(object): def unload(self): + """Unloads the shader program. """ gl.glDisable(arbfp.GL_FRAGMENT_PROGRAM_ARB) gl.glDisable(arbvp.GL_VERTEX_PROGRAM_ARB) @@ -100,6 +198,12 @@ class ARBPShader(object): 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). + """ pos = self.vertParamPositions[name] value = np.array(value, dtype=np.float32).reshape((-1, 4)) @@ -111,7 +215,9 @@ class ARBPShader(object): def setFragParam(self, name, value): - + """Sets the value of the specified vertex program parameter. See + :meth:`setVertParam` for infomration about possible values. + """ pos = self.fragParamPositions[name] value = np.array(value, dtype=np.float32).reshape((-1, 4)) @@ -122,7 +228,12 @@ class ARBPShader(object): def setAttr(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) @@ -133,6 +244,9 @@ class ARBPShader(object): 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) @@ -142,6 +256,20 @@ class ARBPShader(object): 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 = {} @@ -179,8 +307,9 @@ class ARBPShader(object): def __compile(self, vertSrc, fragSrc): - """Compiles the vertex and fragment programs and returns references - to the compiled programs. + """Called by :meth:`__init__`. Compiles the vertex and fragment + programs and returns references to the compiled programs. + """ gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) diff --git a/fsl/fsleyes/gl/shaders/glsl/program.py b/fsl/fsleyes/gl/shaders/glsl/program.py index 0537fa386..3bfe11452 100644 --- a/fsl/fsleyes/gl/shaders/glsl/program.py +++ b/fsl/fsleyes/gl/shaders/glsl/program.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# program.py - Class which encapsulates a GLSL shader program. +# program.py - The GLSLShader class. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # @@ -40,7 +40,7 @@ class GLSLShader(object): a GLSL 1.20 shader program, comprising a vertex shader and a fragment shader. It provides methods to set shader attribute and uniform values, to configure attributes, and to load/unload the program. Furthermore, - the ``GLSLShader`` makes sure that all uniform and attribut variables + the ``GLSLShader`` makes sure that all uniform and attribute variables are converted to the appropriate type. The following methods are available on a ``GLSLShader``: -- GitLab