GLSL Programming/Blender/Multiple Lights
This tutorial covers lighting by multiple light sources.
This tutorial is an extension of the tutorial on smooth specular highlights. If you haven't read that tutorial, you should read it first.
Multiple Lights
editIn the previous tutorials on diffuse reflection, etc., we have only used one light source, which was specified by the uniform gl_LightSource[0]
. However, gl_LightSource
is actually an array of up to gl_MaxLights
light sources.
Here, we take a certain number of these lights (up to gl_MaxLights
) into account and add their contributions to the total lighting. This can be computed with a for-loop in the fragment shader of the tutorial on smooth specular highlights. The structure is this:
const int numberOfLights = gl_MaxLights;
// up to gl_MaxLights (often 8)
...
// initialize total lighting with ambient lighting
vec3 totalLighting = vec3(gl_LightModel.ambient)
* vec3(gl_FrontMaterial.emission);
for (int index = 0; index < numberOfLights; index++)
// for all light sources
{
... compute diffuseReflection and specularReflection
for gl_LightSource[index] ...
totalLighting = totalLighting + diffuseReflection
+ specularReflection;
}
gl_FragColor = vec4(totalLighting, 1.0);
...
The total lighting by all lights is accumulated in totalLighting
by initializing it to the ambient lighting and then adding the diffuse and specular reflection of each light to the previous value of totalLighting
at the end of the for-loop. A for-loop should be familiar to any C/C++/Java/JavaScript programmer. Note that for-loops are sometimes severely limited. Specifically, sometimes you cannot even use uniforms to determine the limits. (The technical reason for the limitation is that the limits have to be known at compile time in order to “un-roll” the loop. gl_MaxLights
is an exception to the rule that uniforms are not allowed as limits because it is one of the built-in uniforms that is constant for all shaders on a certain GPU, i.e. its value is known if the shader is compiled for a specific GPU.)
Complete Shader Code
editThe vertex shader is the same as in the tutorial on smooth specular highlights:
varying vec4 position;
// position of the vertex (and fragment) in view space
varying vec3 varyingNormalDirection;
// surface normal vector in view space
void main()
{
position = gl_ModelViewMatrix * gl_Vertex;
varyingNormalDirection =
normalize(gl_NormalMatrix * gl_Normal);
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
The only tricky thing about the fragment shader is that the nesting of loops and conditionals appears to be limited on some GPUs and/or graphics drivers. In fact, I had to reduce the nesting by avoiding if-else-statements in order to compile the shader on my computer (the OpenGL API reported an internal error). In principle, nesting of conditionals and loops is allowed in GLSL; however, it is probably a good idea to test shaders that make extensive use of nested loops and conditionals on several systems.
Be sure to disable as many default lights through the Preferences within Blender. This will prevent confusion when using less than three light sources.
const int numberOfLights = gl_MaxLights;
// up to gl_MaxLights (often 8)
varying vec4 position;
// position of the vertex (and fragment) in view space
varying vec3 varyingNormalDirection;
// surface normal vector in view space
void main()
{
vec3 normalDirection = normalize(varyingNormalDirection);
vec3 viewDirection = -normalize(vec3(position));
vec3 lightDirection = vec3(0.0, 0.0, 0.0);
float attenuation = 1.0;
// initialize total lighting with ambient lighting
vec3 totalLighting = vec3(gl_LightModel.ambient)
* vec3(gl_FrontMaterial.emission);
for (int index = 0; index < numberOfLights; index++)
// for all light sources
{
if (0.0 == gl_LightSource[index].position.w)
// directional light?
{
attenuation = 1.0; // no attenuation
lightDirection =
normalize(vec3(gl_LightSource[index].position));
}
if (0.0 != gl_LightSource[index].position.w
&& gl_LightSource[index].spotCutoff > 90.0)
// point light?
{
vec3 positionToLightSource =
vec3(gl_LightSource[index].position - position);
float distance = length(positionToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(positionToLightSource);
}
if (0.0 != gl_LightSource[index].position.w
&& gl_LightSource[index].spotCutoff <= 90.0)
// spotlight?
{
vec3 positionToLightSource =
vec3(gl_LightSource[index].position - position);
float distance = length(positionToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(positionToLightSource);
float clampedCosine = max(0.0, dot(-lightDirection,
gl_LightSource[0].spotDirection));
if (clampedCosine < gl_LightSource[0].spotCosCutoff)
// outside of spotlight cone?
{
attenuation = 0.0;
}
else
{
attenuation = attenuation * pow(clampedCosine,
gl_LightSource[0].spotExponent);
}
}
vec3 diffuseReflection = attenuation
* vec3(gl_LightSource[index].diffuse)
* vec3(gl_FrontMaterial.emission)
* max(0.0, dot(normalDirection, lightDirection));
vec3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = vec3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
specularReflection = attenuation
* vec3(gl_LightSource[index].specular)
* vec3(gl_FrontMaterial.specular)
* pow(max(0.0, dot(reflect(-lightDirection,
normalDirection), viewDirection)),
gl_FrontMaterial.shininess);
}
totalLighting = totalLighting + diffuseReflection
+ specularReflection;
}
gl_FragColor = vec4(totalLighting, 1.0);
}
Summary
editCongratulations, you have reached the end of this tutorial. We have seen:
- How a for-loop can be used in GLSL to compute the lighting of multiple lights in a fragment shader.
Further Reading
editIf you still want to know more
- about other parts of the shader code, you should read the tutorial on smooth specular highlights.