GLSL Programming/Unity/Multiple Lights

This tutorial covers lighting by multiple light sources in one pass. In particular, it covers Unity's so-called “vertex lights” in the ForwardBase pass.

Multiple subway lights of limited range in a tunnel.

This tutorial is an extension of Section “Smooth Specular Highlights”. If you haven't read that tutorial, you should read it first.

Multiple Lights in One Pass edit

As discussed in Section “Diffuse Reflection”, Unity's forward rendering path uses separate passes for the most important light sources. These are called “pixel lights” because the built-in shaders render them with per-pixel lighting. All light sources with the Render Mode set to Important are rendered as pixel lights. If the Pixel Light Count of the Quality project settings allows for more pixel lights, then some of the light sources with Render Mode set to Auto are also rendered as pixel lights. What happens to the other light sources? The built-in shaders of Unity render four additional lights as vertex lights in the ForwardBase pass. As the name indicates, the built-in shaders render these lights with per-vertex lighting. This is what this tutorial is about. (Further lights are approximated by spherical harmonic lighting, which is not covered here.)

Unfortunately, it is somewhat unclear how to access the four vertex lights (i.e. their positions and colors). Here is, what appears to work in Unity 3.4 on Windows and MacOS X:

   // Built-in uniforms for "vertex lights"
   uniform vec4 unity_LightColor[4];
      // array of the colors of the 4 light sources 
   uniform vec4 unity_4LightPosX0; 
      // x coordinates of the 4 light sources in world space
   uniform vec4 unity_4LightPosY0; 
      // y coordinates of the 4 light sources in world space
   uniform vec4 unity_4LightPosZ0; 
      // z coordinates of the 4 light sources in world space
   uniform vec4 unity_4LightAtten0; 
      // scale factors for attenuation with squared distance
   // uniform vec4 unity_LightPosition[4] is apparently not 
   // always correctly set in Unity 3.4
   // uniform vec4 unity_LightAtten[4] is apparently not 
   // always correctly set in Unity 3.4

Depending on your platform and version of Unity you might have to use unity_LightPosition[4] instead of unity_4LightPosX0, unity_4LightPosY0, and unity_4LightPosZ0. Similarly, you might have to use unity_LightAtten[4] instead of unity_4LightAtten0. Note what's not available: neither any cookie texture nor the transformation to light space (and therefore neither the direction of spotlights). Also, no 4th component of the light positions is available; thus, it is unclear whether a vertex light is a directional light, a point light, or a spotlight.

Here, we follow Unity's built-in shaders and only compute the diffuse reflection by vertex lights using per-vertex lighting. This can be computed with the following for-loop inside the vertex shader:

            vertexLighting = vec3(0.0, 0.0, 0.0);
            for (int index = 0; index < 4; index++)
            {    
               vec4 lightPosition = vec4(unity_4LightPosX0[index], 
                  unity_4LightPosY0[index], 
                  unity_4LightPosZ0[index], 1.0);
 
               vec3 vertexToLightSource = 
                   vec3(lightPosition - position);        
               vec3 lightDirection = normalize(vertexToLightSource);
               float squaredDistance = 
                  dot(vertexToLightSource, vertexToLightSource);
               float attenuation = 1.0 / (1.0  + 
                   unity_4LightAtten0[index] * squaredDistance);
               vec3 diffuseReflection = 
                  attenuation * vec3(unity_LightColor[index]) 
                  * vec3(_Color) * max(0.0, 
                  dot(varyingNormalDirection, lightDirection));         
 
               vertexLighting = vertexLighting + diffuseReflection;
            }

The total diffuse lighting by all vertex lights is accumulated in vertexLighting by initializing it to black and then adding the diffuse reflection of each vertex light to the previous value of vertexLighting 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; in particular the limits (here: 0 and 4) have to be constants in Unity, i.e. you cannot even use uniforms to determine the limits. (The technical reason is that the limits have to be known at compile time in order to “un-roll” the loop.)

This is more or less how vertex lights are computed in Unity's built-in shaders. However, remember that nothing would stop you from computing specular reflection or per-pixel lighting with these “vertex lights”.

Complete Shader Code edit

In the context of the shader code from Section “Smooth Specular Highlights”, the complete shader code is:

Shader "GLSL per-pixel lighting with vertex lights" {
   Properties {
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
      _SpecColor ("Specular Material Color", Color) = (1,1,1,1) 
      _Shininess ("Shininess", Float) = 10
   }
   SubShader {
      Pass {      
         Tags { "LightMode" = "ForwardBase" } // pass for 
            // 4 vertex lights, ambient light & first pixel light
 
         GLSLPROGRAM
         #pragma multi_compile_fwdbase 
 
         // User-specified properties
         uniform vec4 _Color; 
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
 
         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
 
         // Built-in uniforms for "vertex lights"
         uniform vec4 unity_LightColor[4];
         uniform vec4 unity_4LightPosX0; 
            // x coordinates of the 4 light sources in world space
         uniform vec4 unity_4LightPosY0; 
            // y coordinates of the 4 light sources in world space
         uniform vec4 unity_4LightPosZ0; 
            // z coordinates of the 4 light sources in world space
         uniform vec4 unity_4LightAtten0; 
            // scale factors for attenuation with squared distance

         // Varyings         
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
         varying vec3 vertexLighting;
 
         #ifdef VERTEX
 
         void main()
         {                                            
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
 
            position = modelMatrix * gl_Vertex;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
 
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
 
            // Diffuse reflection by four "vertex lights"            
            vertexLighting = vec3(0.0, 0.0, 0.0);
            #ifdef VERTEXLIGHT_ON
            for (int index = 0; index < 4; index++)
            {    
               vec4 lightPosition = vec4(unity_4LightPosX0[index], 
                  unity_4LightPosY0[index], 
                  unity_4LightPosZ0[index], 1.0);
 
               vec3 vertexToLightSource = 
                  vec3(lightPosition - position);        
               vec3 lightDirection = normalize(vertexToLightSource);
               float squaredDistance = 
                  dot(vertexToLightSource, vertexToLightSource);
               float attenuation = 1.0 / (1.0 + 
                  unity_4LightAtten0[index] * squaredDistance);
               vec3 diffuseReflection =  
                  attenuation * vec3(unity_LightColor[index]) 
                  * vec3(_Color) * max(0.0, 
                  dot(varyingNormalDirection, lightDirection));         
 
               vertexLighting = vertexLighting + diffuseReflection;
            }
            #endif
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection); 
            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection;
            float attenuation;
 
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
 
            vec3 ambientLighting = 
                vec3(gl_LightModel.ambient) * vec3(_Color);
 
            vec3 diffuseReflection = 
               attenuation * vec3(_LightColor0) * vec3(_Color) 
               * 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(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            gl_FragColor = vec4(vertexLighting + ambientLighting 
               + diffuseReflection + specularReflection, 1.0);
         }
 
         #endif
 
         ENDGLSL
      }
 
      Pass {      
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional "pixel lights"
         Blend One One // additive blending 
 
         GLSLPROGRAM
 
         // User-specified properties
         uniform vec4 _Color; 
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
  
         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
 
         // Varyings
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
 
         #ifdef VERTEX
 
         void main()
         {                                
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
 
            position = modelMatrix * gl_Vertex;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
 
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
 
            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection;
            float attenuation;
 
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
 
            vec3 diffuseReflection = 
               attenuation * vec3(_LightColor0) * vec3(_Color) 
               * 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(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            gl_FragColor = vec4(diffuseReflection 
               + specularReflection, 1.0);
         }
 
         #endif
 
         ENDGLSL
      }
   } 
   // The definition of a fallback shader should be commented out 
   // during development:
   // Fallback "Specular"
}

The use of #pragma multi_compile_fwdbase and #ifdef VERTEXLIGHT_ON ... #endif appears to be necessary to make sure that no vertex lighting is computed when Unity doesn't provide the data.

Summary edit

Congratulations, you have reached the end of this tutorial. We have seen:

  • How Unity's vertex lights are specified.
  • How a for-loop can be used in GLSL to compute the lighting of multiple lights in one pass.

Further Reading edit

If you still want to know more


< GLSL Programming/Unity

Unless stated otherwise, all example source code on this page is granted to the public domain.