Introduction to Shaders: Multiple Lights, GLSL Arrays and Structures
#! /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
from OpenGLContext.scenegraph.basenodes import Sphere
class TestContext( BaseContext ):
"""Demonstrates use of attribute types in GLSL
"""
def OnInit( self ):
"""Initialize the context"""
GLSL Structures
We have previously been using values named Material_ambient,
Material_diffuse, etceteras to specify our Material's properties.
GLSL allows us to bind these kinds of values together into a
structure. The structure doesn't provide many benefits other
than keeping the namespaces of your code clean and allowing for
declaring multiple uniforms of the same type, such as a "front"
and "back" material.
We are going to define a very simple Material struct which is
a subset of the built-in gl_MaterialParameters structure
(which also has an "emission" parameter). GLSL defines two
built-in Material uniforms gl_FrontMaterial and gl_BackMaterial.
It is possible (though seldom done) to fill in these uniform
values with glUniform calls rather than the legacy glMaterial
calls.
materialStruct = """
struct Material {
vec4 ambient;
vec4 diffuse;
vec4 specular;
float shininess;
};
"""
Note that each sub-element must be terminated with a semi-colon
';' character, and that qualifiers (in, out, uniform, etceteras)
are not allowed within the structure definition. This statement
has to occur *before* any use of the structure to declare a
variable as being of this type.
Our light-weighting code has not changed from the previous
tutorial. It is still a Blinn-Phong calculation based on the
half-vector of light and view vector.
phong_weightCalc = """
vec2 phong_weightCalc(
in vec3 light_pos, // light position
in vec3 half_light, // half-way vector between light and view
in vec3 frag_normal, // geometry normal
in float shininess
) {
// returns vec2( ambientMult, diffuseMult )
float n_dot_pos = max( 0.0, dot(
frag_normal, light_pos
));
float n_dot_half = 0.0;
if (n_dot_pos > -.05) {
n_dot_half = pow(max(0.0,dot(
half_light, frag_normal
)), shininess);
}
return vec2( n_dot_pos, n_dot_half);
}
"""
Our vertex shader is also unchanged. We could move many
of the operations currently done in our fragment shader here
to reduce the processing load for our shader.
vertex = shaders.compileShader(
"""
attribute vec3 Vertex_position;
attribute vec3 Vertex_normal;
varying vec3 baseNormal;
void main() {
gl_Position = gl_ModelViewProjectionMatrix * vec4(
Vertex_position, 1.0
);
baseNormal = gl_NormalMatrix * normalize(Vertex_normal);
}""", GL_VERTEX_SHADER)
To create a uniform with a structure type, we simply use
the structure as the data-type declaration for the uniform.
As opposed to using e.g. vec4 or vec3, we use Material (our
structure name defined above) and give the uniform a name.
"""uniform Material material;"""
GLSL Arrays
Each light we have defined (so far) is composed to 4 4-component
vectors, ambient, diffuse and specular colour, along with the
"position" (direction) vector. If we wanted to provide, for
instance, 3 lights of this type, we *could* create 12 different
uniform values, and set each of these uniforms individually.
GLSL, however, provides for array data-types. The array types
must be "sized" (have a specific, final size) in order to be
usable, so no "pointer" types are available, but we don't need
them for this type of operation. We can define a "lights"
uniform which is declared as a sized array of 12 vec4 elements:
"""uniform vec4 lights[ 12 ];"""
And we can loop over "lights" using 0-indexed [i] indices,
where i must be an integer. The for loop will be familiar to
those who have used C looping:
"""for (i=0;i<12;i=i+4) { blah; }"""
Note that you must declare the iterator variable ("i" here)
We iterate over each light in our array of lights accumulating
the results into the fragColor variable we've defined. The
global component is used to initialize the variable, with the
contribution of each light added to the result.
fragment = shaders.compileShader(
phong_weightCalc + materialStruct + """
uniform Material material;
uniform vec4 Global_ambient;
uniform vec4 lights[ 12 ]; // 3 possible lights 4 vec4's each
varying vec3 baseNormal;
void main() {
vec4 fragColor = Global_ambient * material.ambient;
int AMBIENT = 0;
int DIFFUSE = 1;
int SPECULAR = 2;
int POSITION = 3;
int i;
for (i=0;i<12;i=i+4) {
// normalized eye-coordinate Light location
vec3 EC_Light_location = normalize(
gl_NormalMatrix * lights[i+POSITION].xyz
);
// half-vector calculation
vec3 Light_half = normalize(
EC_Light_location - vec3( 0,0,-1 )
);
vec2 weights = phong_weightCalc(
EC_Light_location,
Light_half,
baseNormal,
material.shininess
);
fragColor = (
fragColor
+ (lights[i+AMBIENT] * material.ambient)
+ (lights[i+DIFFUSE] * material.diffuse * weights.x)
+ (lights[i+SPECULAR] * material.specular * weights.y)
);
}
gl_FragColor = fragColor;
}
""", GL_FRAGMENT_SHADER)
Why not an Array of Structures?
Originally this tutorial was going to use an array of LightSource
structures as a Uniform, with the components of the structures
specified with separate calls to glUniform4f. Problem is, that
doesn't actually *work*. While glUniform *should* be able to
handle array-of-structure indexing, it doesn't actually support
this type of operation in the real world. The built-in
gl_LightSourceParameters are an array-of-structures, but
apparently the GL implementations consider this a special case,
rather than a generic type of functionality to be supported.
An array-of-structures value looks like this when declared in GLSL:
lightStruct = """
// NOTE: this does not work, it compiles, but you will
// not be able to fill in the individual members...
struct LightSource {
vec4 ambient;
vec4 diffuse;
vec4 specular;
vec4 position;
};
uniform LightSource lights[3];
"""
When you attempt to retrieve the location for the Uniform
via:
glGetUniformLocation( shader, 'lights[0].ambient' )
you will always get a -1 (invalid) location.
OpenGL 3.1 introduced the concept of Uniform Buffers, which allow
for packing Uniform data into VBO storage, but it's not yet clear
whether they will support array-of-structure specification.
self.shader = shaders.compileProgram(vertex,fragment)
self.coords,self.indices,self.count = Sphere(
radius = 1
).compile()
self.uniform_locations = {}
for uniform,value in self.UNIFORM_VALUES:
location = glGetUniformLocation( self.shader, uniform )
if location in (None,-1):
print 'Warning, no uniform: %s'%( uniform )
self.uniform_locations[uniform] = location
There's no real reason to treat the "lights" uniform specially,
other than that we want to call attention to it. We get the
uniform as normal. Note that we *could* also retrieve a
sub-element of the array by specifying 'lights[3]' or the like.
self.uniform_locations['lights'] = glGetUniformLocation(
self.shader, 'lights'
)
for attribute in (
'Vertex_position','Vertex_normal',
):
location = glGetAttribLocation( self.shader, attribute )
if location in (None,-1):
print 'Warning, no attribute: %s'%( uniform )
setattr( self, attribute+ '_loc', location )
Our individually-specified uniform values
UNIFORM_VALUES = [
('Global_ambient',(.05,.05,.05,1.0)),
('material.ambient',(.2,.2,.2,1.0)),
('material.diffuse',(.5,.5,.5,1.0)),
('material.specular',(.8,.8,.8,1.0)),
('material.shininess',(.995,)),
]
The parameters we use to specify our lights, note that
the first item in the tuples is dropped, it is the value
that *should* work in glGetUniformLocation, but does not.
What actually gets passed in is a single float array with
12 4-float values representing all of the data-values for
all of the enabled lights.
You'll notice that we're using 0.0 as the 'w' coordinate
for the light positions. We're using this to flag that the
position is actually a *direction*. This will become useful
in later tutorials where we have multiple light-types.
LIGHTS = array([
x[1] for x in [
('lights[0].ambient',(.05,.05,.05,1.0)),
('lights[0].diffuse',(.3,.3,.3,1.0)),
('lights[0].specular',(1.0,0.0,0.0,1.0)),
('lights[0].position',(4.0,2.0,10.0,0.0)),
('lights[1].ambient',(.05,.05,.05,1.0)),
('lights[1].diffuse',(.3,.3,.3,1.0)),
('lights[1].specular',(0.0,1.0,0.0,1.0)),
('lights[1].position',(-4.0,2.0,10.0,0.0)),
('lights[2].ambient',(.05,.05,.05,1.0)),
('lights[2].diffuse',(.3,.3,.3,1.0)),
('lights[2].specular',(0.0,0.0,1.0,1.0)),
('lights[2].position',(-4.0,2.0,-10.0,0.0)),
]
], 'f')
def Render( self, mode = None):
"""Render the geometry for the scene."""
BaseContext.Render( self, mode )
glUseProgram(self.shader)
try:
self.coords.bind()
self.indices.bind()
stride = self.coords.data[0].nbytes
try:
Here's our only change to the rendering process,
we pass in the entire array of light-related data with
a single call to glUniform4fv. The 'v' forms of
glUniform all allow for passing arrays of values,
and all require that you specify the number of elements
being passed (here 12).
Aside: Incidentally, Uniforms are actually stored with
the shader until the shader is re-linked, so specifying
the uniforms on each rendering pass (as we do here) is
not necessary. The shader merely needs to be "in use"
during the glUniform call, and this is a convenient,
if inefficient, way to ensure it is in use at the time
we are calling glUniform.
glUniform4fv(
self.uniform_locations['lights'],
12,
self.LIGHTS
)
test_lights = (GLfloat * 12)()
glGetUniformfv( self.shader, self.uniform_locations['lights'], test_lights )
print 'Lights', list(test_lights)
for uniform,value in self.UNIFORM_VALUES:
location = self.uniform_locations.get( uniform )
if location not in (None,-1):
if len(value) == 4:
glUniform4f( location, *value )
elif len(value) == 3:
glUniform3f( location, *value )
elif len(value) == 1:
glUniform1f( location, *value )
glEnableVertexAttribArray( self.Vertex_position_loc )
glEnableVertexAttribArray( self.Vertex_normal_loc )
glVertexAttribPointer(
self.Vertex_position_loc,
3, GL_FLOAT,False, stride, self.coords
)
glVertexAttribPointer(
self.Vertex_normal_loc,
3, GL_FLOAT,False, stride, self.coords+(5*4)
)
glDrawElements(
GL_TRIANGLES, self.count,
GL_UNSIGNED_SHORT, self.indices
)
finally:
self.coords.unbind()
self.indices.unbind()
glDisableVertexAttribArray( self.Vertex_position_loc )
glDisableVertexAttribArray( self.Vertex_normal_loc )
finally:
glUseProgram( 0 )
if __name__ == "__main__":
TestContext.ContextMainLoop()
Our next tutorial will optimize the directional light code here
so that it is less wasteful of GPU resources.
Multiple Lights, GLSL Arrays and Structures