GLSL Programming/Blender/Multiple Lights

Multiple subway lights of limited range in a tunnel.

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 LightsEdit

In 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 CodeEdit

The 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);
         }

SummaryEdit

Congratulations, 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 ReadingEdit

If you still want to know more


< GLSL Programming/Blender

Unless stated otherwise, all example source code on this page is granted to the public domain.
Last modified on 23 September 2012, at 22:04