Jump to content

GLSL Programming/Unity/Cookies

From Wikibooks, open books for an open world
Illustration of a gobo in front of a light source.
A cookie in action: similar to a gobo but not as close to the light source.

This tutorial covers projective texture mapping in light space, which is useful to implement cookies for spotlights and directional light sources. (In fact, Unity uses a built-in cookie for any spotlight.)

The tutorial is based on the code of Section “Smooth Specular Highlights” and Section “Transparent Textures”. If you haven't read those tutorials yet, you should read them first.

Gobos and Cookies in Real Life

[edit | edit source]

In real life, gobos are pieces of material (often metal) with holes that are placed in front of light sources to manipulate the shape of light beams or shadows. Cookies (or “cuculoris”) serve a similar purpose but are placed at a larger distance from the light source as shown in the image to the left.

Unity's Cookies

[edit | edit source]

In Unity, a cookie can be specified for each light source in the Inspector View when the light source is selected. This cookie is basically an alpha texture map (see Section “Transparent Textures”) that is placed in front of the light source and moves with it (therefore it is actually similar to a gobo). It lets light pass through where the alpha component of the texture image is 1 and blocks light where the alpha component is 0. Unity's cookies for spotlights and directional lights are just square, two-dimensional alpha texture maps. On the other hand, cookies for point lights are cube maps, which we will not cover here.

In order to implement a cookie, we have to extend the shader of any surface that should be affected by the cookie. (This is very different from how Unity's projectors work; see Section “Projectors”.) Specifically, we have to attenuate the light of each light source according to its cookie in the lighting computation of a shader. Here, we use the per-pixel lighting described in Section “Smooth Specular Highlights”; however, the technique can be applied to any lighting computation.

In order to find the relevant position in the cookie texture, the position of the rasterized point of a surface is transformed into the coordinate system of the light source. This coordinate system is very similar to the clip coordinate system of a camera, which is described in Section “Vertex Transformations”. In fact, the best way to think of the coordinate system of a light source is probably to think of the light source as a camera. The x and y light coordinates are then related to the screen coordinates of this hypothetical camera. Transforming a point from world coordinates to light coordinates is actually very easy because Unity provides the required 4×4 matrix as the uniform variable _LightMatrix0. (Otherwise we would have to set up the matrix similar to the matrices for the viewing transformation and the projection, which are discussed in Section “Vertex Transformations”.)

For best efficiency, the transformation of the surface point from world space to light space should be performed by multiplying _LightMatrix0 to the position in world space in the vertex shader, for example this way:

         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)
         uniform mat4 _LightMatrix0; // transformation 
            // from world to light space (from Autolight.cginc)
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec4 positionInLightSpace; 
            // position of the vertex (and fragment) in light 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;
            positionInLightSpace = _LightMatrix0 * position;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
            
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif

Apart from the definitions of the uniform _LightMatrix0 and the varying positionInLightSpace and the instruction to compute positionInLightSpace, this is the same vertex shader as in Section “Smooth Specular Highlights”.

Cookies for Directional Light Sources

[edit | edit source]

For the cookie of a directional light source, we can just use the x and y light coordinates in positionInLightSpace as texture coordinates for a lookup in the cookie texture _LightTexture0. This texture lookup should be performed in the fragment shader. Then the resulting alpha component should be multiplied to the computed lighting; for example:

            // compute diffuseReflection and specularReflection

            float cookieAttenuation = 1.0;
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               cookieAttenuation = texture2D(_LightTexture0, 
                  vec2(positionInLightSpace)).a;
            }
            // compute cookieAttenuation for spotlights here

            gl_FragColor = vec4(cookieAttenuation 
               * (diffuseReflection + specularReflection), 1.0);

Instead of vec2(positionInLightSpace) we could also use positionInLightSpace.xy to get a two-dimensional vector with the x and y coordinates in light space.

Cookies for Spotlights

[edit | edit source]

For spotlights, the x and y light coordinates in positionInLightSpace have to be divided by the w light coordinate. This division is characteristic for projective texture mapping and corresponds to the perspective division for a camera, which is described in Section “Vertex Transformations”. Unity defines the matrix _LightMatrix0 such that we have to add to both coordinates after the division:

               cookieAttenuation = texture2D(_LightTexture0, 
                  vec2(positionInLightSpace) / positionInLightSpace.w 
                  + vec2(0.5)).a;

For some GPUs it might be more efficient to use the built-in function texture2DProj, which takes three texture coordinates in a vec3 and divides the first two coordinates by the third coordinate before the texture lookup. A problem with this approach is that we have to add after the division by positionInLightSpace.w; however, texture2DProj doesn't allow us to add anything after the internal division by the third texture coordinate. The solution is to add 0.5 * positionInLightSpace.w before the division by positionInLightSpace.w, which corresponds to adding after the division:

               vec3 textureCoords = vec3(vec2(positionInLightSpace) 
                  + vec2(0.5 * positionInLightSpace.w), 
                  positionInLightSpace.w);
               cookieAttenuation = 
                  texture2DProj(_LightTexture0, textureCoords).a;

Note that the texture lookup for directional lights can also be implemented with texture2DProj by setting textureCoords to vec3(vec2(positionInLightSpace), 1.0). This would allow us to use only one texture lookup for both directional lights and for spotlights, which is more efficient on some GPUs.

Complete Shader Code

[edit | edit source]

For the complete shader code we use a simplified version of the ForwardBase pass of Section “Smooth Specular Highlights” since Unity only uses a directional light without cookie in the ForwardBase pass. All light sources with cookies are handled by the ForwardAdd pass. We ignore cookies for point lights, for which _LightMatrix0[3][3] is 1.0 (but we include them in the next section). Spotlights always have a cookie texture: if the user didn't specify one, Unity supplies a cookie texture to generate the shape of a spotlight; thus, it is OK to always apply the cookie. Directional lights don't always have a cookie; however, if there is only one directional light source without cookie then it has been processed in the ForwardBase pass. Thus, unless there are more than one directional light sources without cookies, we can assume that all directional light sources in the ForwardAdd pass have cookies. In this case, the complete shader code could be:

Shader "GLSL per-pixel lighting with cookies" {
   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 ambient light 
            // and first directional light source without cookie
 
         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)
 
         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 = 
               normalize(vec3(_WorldSpaceLightPos0));

            vec3 ambientLighting = 
               vec3(gl_LightModel.ambient) * vec3(_Color);
 
            vec3 diffuseReflection = 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 = vec3(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            gl_FragColor = vec4(ambientLighting 
               + diffuseReflection + specularReflection, 1.0);
         }
 
         #endif
 
         ENDGLSL
      }
 
      Pass {      
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources
         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)
         
         uniform mat4 _LightMatrix0; // transformation 
            // from world to light space (from Autolight.cginc)
         uniform sampler2D _LightTexture0; 
            // cookie alpha texture map (from Autolight.cginc)
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec4 positionInLightSpace; 
            // position of the vertex (and fragment) in light 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;
            positionInLightSpace = _LightMatrix0 * position;
            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);
            }
 
            float cookieAttenuation = 1.0;
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               cookieAttenuation = texture2D(_LightTexture0, 
                  vec2(positionInLightSpace)).a;
            }
            else if (1.0 != _LightMatrix0[3][3]) 
               // spotlight (i.e. not a point light)?
            {
               cookieAttenuation = texture2D(_LightTexture0, 
                  vec2(positionInLightSpace) / positionInLightSpace.w 
                  + vec2(0.5)).a;
            }
            gl_FragColor = vec4(cookieAttenuation 
               * (diffuseReflection + specularReflection), 1.0);
        }
 
         #endif
 
         ENDGLSL
      }
   } 
   // The definition of a fallback shader should be commented out 
   // during development:
   // Fallback "Specular"
}

Shader Programs for Specific Light Sources

[edit | edit source]

The previous shader code is limited to scenes with at most one directional light source without a cookie. Also, it doesn't take cookies of point light sources into account. Writing more general shader code requires different ForwardAdd passes for different light sources. (Remember that the light source in the ForwardBase pass is always a directional light source without cookie.) Fortunately, Unity offers a way to generate multiple shaders by using the following Unity-specific directive (right after GLSLPROGRAM in the ForwardAdd pass):

#pragma multi_compile_lightpass

With this instruction, Unity will compile the shader code for the ForwardAdd pass multiple times for different kinds of light sources. Each compilation is distinguished by the definition of one of the following symbols: DIRECTIONAL, DIRECTIONAL_COOKIE, POINT, POINT_NOATT, POINT_COOKIE, SPOT. The shader code should check which symbol is defined (using the directives #if defined ... #elif defined ... #endif) and include appropriate instructions. For example:

Shader "GLSL per-pixel lighting with cookies" {
   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 ambient light 
            // and first directional light source without cookie
 
         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)
 
         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 = 
               normalize(vec3(_WorldSpaceLightPos0));
 
            vec3 ambientLighting = 
               vec3(gl_LightModel.ambient) * vec3(_Color);
 
            vec3 diffuseReflection = 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 = vec3(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            gl_FragColor = vec4(ambientLighting 
               + diffuseReflection + specularReflection, 1.0);
         }
 
         #endif
 
         ENDGLSL
      }
 
      Pass {      
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources
         Blend One One // additive blending 
 
         GLSLPROGRAM
 
         #pragma multi_compile_lightpass
 
         // 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)
 
         uniform mat4 _LightMatrix0; // transformation 
            // from world to light space (from Autolight.cginc)
         #if defined DIRECTIONAL_COOKIE || defined SPOT
            uniform sampler2D _LightTexture0; 
               // cookie alpha texture map (from Autolight.cginc)
         #elif defined POINT_COOKIE
            uniform samplerCube _LightTexture0; 
               // cookie alpha texture map (from Autolight.cginc)
         #endif
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec4 positionInLightSpace; 
            // position of the vertex (and fragment) in light 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;
            positionInLightSpace = _LightMatrix0 * position;
            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 = 1.0; 
               // by default no attenuation with distance
 
            #if defined DIRECTIONAL || defined DIRECTIONAL_COOKIE
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            #elif defined POINT_NOATT
               lightDirection = 
                  normalize(vec3(_WorldSpaceLightPos0 - position));
            #elif defined POINT || defined POINT_COOKIE || defined SPOT
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            #endif
 
            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);
            }
 
            float cookieAttenuation = 1.0; 
               // by default no cookie attenuation
            #if defined DIRECTIONAL_COOKIE
               cookieAttenuation = texture2D(_LightTexture0, 
                  vec2(positionInLightSpace)).a;
            #elif defined POINT_COOKIE
               cookieAttenuation = textureCube(_LightTexture0, 
                  vec3(positionInLightSpace)).a;
            #elif defined SPOT
               cookieAttenuation = texture2D(_LightTexture0, 
                  vec2(positionInLightSpace) 
                  / positionInLightSpace.w + vec2(0.5)).a;
            #endif
            gl_FragColor = vec4(cookieAttenuation * 
               (diffuseReflection + specularReflection), 1.0);
         }
 
         #endif
 
         ENDGLSL
      }
   } 
   // The definition of a fallback shader should be commented out 
   // during development:
   // Fallback "Specular"
}

Note that the cookie for a point light source is using a cube texture map. This kind of texture map is discussed in Section “Reflecting Surfaces”.

Summary

[edit | edit source]

Congratulations, you have learned the most important aspects of projective texture mapping. We have seen:

  • How to implement cookies for directional light sources.
  • How to implement spotlights (with and without user-specified cookies).
  • How to implement different shaders for different light sources.

Further Reading

[edit | edit source]

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.