Introduction to Shaders: Varying Values (Colours)
#! /usr/bin/env python
from OpenGLContext import testingcontext
BaseContext = testingcontext.getInteractive()
from OpenGL.GL import *
from OpenGL.arrays import vbo
from OpenGLContext.arrays import *
from OpenGL.GL import shaders
class TestContext( BaseContext ):
"""This shader just passes gl_Color from an input array to
the fragment shader, which interpolates the values across the
face (via a "varying" data type).
"""
def OnInit( self ):
"""Initialize the context once we have a valid OpenGL environ"""
Aside: Compilation Errors
As we get more and more complex shaders, you are inevitably
going to run into situations where your shaders have compilation
errors and need to be debugged. The PyOpenGL convenience
wrappers for shaders will raise a RuntimeError instance when/if
shader compilation fails. The second argument to the RuntimeError
will be the source-code that was being compiled when the failure
occurred. Normally the Python traceback of this error will be
sufficient to help you track down the problem (with the appropriate
references, of course).
try:
shaders.compileShader( """ void main() { """, GL_VERTEX_SHADER )
except (GLError, RuntimeError) as err:
print 'Example of shader compile error', err
else:
raise RuntimeError( """Didn't catch compilation error!""" )
Varying Values
In our previous tutorial, we calculated the colour of each
fragment as a constant colour (green). Now we are going to
make each vertex a different colour and allow the GL to
interpolate between those colours.
We are going to use the legacy OpenGL colour for our vertices,
that is, the colour that would normally be provided to the
legacy (fixed-function) pipeline. This value shows up as the
built-in vec4 "gl_Color". Within the vertex shader, each call
of the vertex shader will have gl_Color assigned.
To communicate the colour for each vertex to the fragment
shader, we need to define a "varying" variable. A varying
variable is interpolated across the triangle for each fragment,
taking the perspectivally correct blended value for the vertices
which make up each corner of the triangle. Thus if we were to
define one vertex as being black and another as white, the
fragments generated for the area between them would fade from
black to white (via grey).
You will note that we define the varying value *outside* the main
function. The varying value can be loosely thought of as being
declared a "global" so that it can be seen in both shaders.
However, the varying value is is being processed by intermediate
clipping and interpolation processes.
vertex = shaders.compileShader(
"""
varying vec4 vertex_color;
void main() {
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
vertex_color = gl_Color;
}""",GL_VERTEX_SHADER)
Our fragment shader, again, declares the vertex_color
varying value. Since we would like the final fragment
colour to be the interpolated colour between our vertices,
we can simply assign vertex_color to gl_FragColor.
fragment = shaders.compileShader("""
varying vec4 vertex_color;
void main() {
gl_FragColor = vertex_color;
}""",GL_FRAGMENT_SHADER)
self.shader = shaders.compileProgram(vertex,fragment)
Our geometry now has two components for every vertex,
the first is the vertex position, which is the same set of
values as we saw in our previous tutorial. The first three
floats in each vertex (row) are the position. The last
three values represent the colours of each vertex. Thus
the triangle (the first three vertices) will blend from
red to yellow to cyan.
As noted in the previous tutorial, this "packed" format
tends to be more efficient on modern hardware than having
separate data-arrays for each type of data.
self.vbo = vbo.VBO(
array( [
[ 0, 1, 0, 0,1,0 ],
[ -1,-1, 0, 1,1,0 ],
[ 1,-1, 0, 0,1,1 ],
[ 2,-1, 0, 1,0,0 ],
[ 4,-1, 0, 0,1,0 ],
[ 4, 1, 0, 0,0,1 ],
[ 2,-1, 0, 1,0,0 ],
[ 4, 1, 0, 0,0,1 ],
[ 2, 1, 0, 0,1,1 ],
],'f')
)
def Render( self, mode):
"""Render the geometry for the scene."""
BaseContext.Render( self, mode )
As before, we need to enable the use of our compiled shaders
and make our VBO active so that array-specification routines
will use the VBO as the source for our geometric data.
glUseProgram(self.shader)
try:
self.vbo.bind()
try:
Since we want to provide both position and colour
arrays to the shader, we need to enable two different
arrays. These two built-in arrays map to the built-in
gl_Vertex and gl_Color "attribute" variables we are
using in our vertex shader.
These "enables" tell OpenGL that for each vertex we
render, we would like to read one record from the
enabled arrays. If we were to do this without specifying
the arrays, OpenGL would likely seg-fault our program
as it tried to access NULL memory locations.
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
We are using the "full" form of the array-definition
calls here, as we want to be able to specify "strides"
across the data-arrays. The arguments to the pointer
definition calls are:
- size -- number of values in each record
- type -- constant defining the type of value for each item in the record
- stride -- number of bytes between the start of each consecutive record, in our case we have 6 32-bit floating-point values in each record, for a total of 4*6 == 24 bytes between records.
- pointer -- reference to the data we wish to use for this array
The vertex pointer is passed a reference to our VBO,
which tells OpenGL to read from the currently-bound VBO.
Under the covers, the VBO wrapper is simply passing a
NULL pointer to the GL.
glVertexPointer(3, GL_FLOAT, 24, self.vbo )
The colour pointer also wants to read data from the
VBO, but it needs to begin reading each record from a
point which is 3 floating-point values (the width of the
position information) after where the position pointer
gets its value.
Since the definition of the array includes the step
between elements, we ask OpenGL to begin calculating the
addresses from which to read the colour information at
the beginning of the current VBO + 12 bytes. Under the
covers, the VBO wrapper is simply passing a void pointer
to the address 12 to the GL.
glColorPointer(3, GL_FLOAT, 24, self.vbo+12 )
We now trigger our drawing operation and cleanup
in the same way as we have seen before.
glDrawArrays(GL_TRIANGLES, 0, 9)
finally:
self.vbo.unbind()
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);
finally:
glUseProgram( 0 )
if __name__ == "__main__":
TestContext.ContextMainLoop()
Terms:
- interpolate -- create new data-values by blending other values
Varying Values (Colours)