GLSL Programming/Blender/Diffuse Reflection

This tutorial covers per-vertex diffuse reflection.

The light reflection from the surface of the moon is (in a good approximation) only diffuse.

It's the first in a series of tutorials about basic lighting in Blender. In this tutorial, we start with diffuse reflection from a single directional light source and then include point lights and spotlights. Further tutorials cover extensions of this, in particular specular reflection, per-pixel lighting, two-sided lighting, and multiple light sources.

Diffuse reflection can be computed using the surface normal vector N and the light vector L, i.e. the vector to the light source.

Diffuse Reflection edit

The moon exhibits almost exclusively diffuse reflection (also called Lambertian reflection), i.e. light is reflected into all directions without specular highlights. Other examples of such materials are chalk and matte paper; in fact, any surface that appears dull and matte.

In the case of perfect diffuse reflection, the intensity of the observed reflected light depends on the cosine of the angle between the surface normal vector and the ray of the incoming light. As illustrated in the figure to the left, it is common to consider normalized vectors starting in the point of a surface, where the lighting should be computed: the normalized surface normal vector N is orthogonal to the surface and the normalized light direction L points to the light source.

For the observed diffuse reflected light  , we need the cosine of the angle between the normalized surface normal vector N and the normalized direction to the light source L, which is the dot product N·L because the dot product a·b of any two vectors a and b is:

 .

In the case of normalized vectors, the lengths |a| and |b| are both 1.

If the dot product N·L is negative, the light source is on the “wrong” side of the surface and we should set the reflection to 0. This can be achieved by using max(0, N·L), which makes sure that the value of the dot product is clamped to 0 for negative dot products. Furthermore, the reflected light depends on the intensity of the incoming light   and a material constant   for the diffuse reflection: for a black surface, the material constant   is 0, for a white surface it is 1. The equation for the diffuse reflected intensity is then:

 

For colored light, this equation applies to each color component (e.g. red, green, and blue). Thus, if the variables  ,  , and   denote color vectors and the multiplications are performed component-wise (which they are for vectors in GLSL), this equation also applies to colored light. This is what we actually use in the shader code.

Shader Code for One Directional Light Source edit

If we have only one directional light source (i.e. a “sun” light in Blender), the shader code for implementing the equation for   is relatively small. In order to implement the equation, we follow the questions about implementing equations, which were discussed in the tutorial on silhouette enhancement:

  • Should the equation be implemented in the vertex shader or the fragment shader? We try the vertex shader here. In the tutorial on smooth specular highlights, we will look at an implementation in the fragment shader.
  • In which coordinate system should the equation be implemented? We try view space by default in Blender. (Which turns out to be a good choice here because Blender provides the light direction in view space.)
  • Where do we get the parameters from? The answer to this is a bit longer:

As described in the tutorial on shading in view space, gl_FrontMaterial.diffuse is usually black; thus, we use gl_FrontMaterial.emission for the diffuse material color  . (Remember to set Shading > Emit to 1 in the Material tab.) The direction to the light source L in view space is available in gl_LightSource[0].position and the light color   is available as gl_LightSource[0].diffuse. (If there is only one light source, it is always the 0th in the array gl_LightSource[].) We get the surface normal vector in object coordinates from the attribute gl_Normal. Since we implement the equation in view space, we have to convert the surface normal vector from object space to view space as discussed in the tutorial on silhouette enhancement.

The vertex shader then looks like this:

         varying vec4 color; 

         void main()
         {                              
            vec3 normalDirection = 
               normalize(gl_NormalMatrix * gl_Normal);
            vec3 lightDirection = 
               normalize(vec3(gl_LightSource[0].position));
 
            vec3 diffuseReflection = 
               vec3(gl_LightSource[0].diffuse) 
               * vec3(gl_FrontMaterial.emission)
               * max(0.0, dot(normalDirection, lightDirection));

            color = vec4(diffuseReflection, 1.0); 
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }

And the fragment shader is:

         varying vec4 color;

         void main()
         {            
            gl_FragColor = color;
         }

When you try this shader, make sure that there is only one light source in the scene, which has to be directional, i.e. of type “Sun”. If there is no light source, you can create a directional light source by selecting Add > Lamp > Sun from the menu of the Info window.

Changes for a Point Light Source edit

In the case of a directional light source gl_LightSource[0].position specifies the direction from where light is coming. In the case of a point light source (or a spot light source), however, gl_LightSource[0].position specifies the position of the light source in view space and we have to compute the direction to the light source as the difference vector from the position of the vertex in view space to the position of the light source. Since the 4th coordinate of a point is 1 and the 4th coordinate of a direction is 0, we can easily distinguish between the two cases:

            vec3 lightDirection;

            if (0.0 == gl_LightSource[0].position.w) 
               // directional light?
            {
               lightDirection = 
                  normalize(vec3(gl_LightSource[0].position));
            } 
            else // point or spot light
            {
               lightDirection = 
                  normalize(vec3(gl_LightSource[0].position 
                  - gl_ModelViewMatrix * gl_Vertex));
            }

While there is no attenuation of light for directional light sources, we should add some attenuation with distance to point and spot light source. As light spreads out from a point in three dimensions, it's covering ever larger virtual spheres at larger distances. Since the surface of these spheres increases quadratically with increasing radius and the total amount of light per sphere is the same, the amount of light per area decreases quadratically with increasing distance from the point light source. Thus, we should divide the intensity of the light source by the squared distance to the vertex.

Since a quadratic attenuation is rather rapid, we use a linear attenuation with distance, i.e. we divide the intensity by the distance instead of the squared distance. The code could be:

            vec3 lightDirection;
            float attenuation;

            if (0.0 == gl_LightSource[0].position.w) 
               // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = 
                  normalize(vec3(gl_LightSource[0].position));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(gl_LightSource[0].position 
                  - gl_ModelViewMatrix * gl_Vertex);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }

Usually, we would multiply the linear attenuation function 1.0 / distance by the uniform gl_LightSource[0].linearAttenuation; however, Blender apparently doesn't set this uniform variable correctly. In fact, if Blender would set the uniforms correctly, we should compute the attenuation this way:

               attenuation = 
                  1.0 / (gl_LightSource[0].constantAttenuation 
                  + gl_LightSource[0].linearAttenuation * distance
                  + gl_LightSource[0].quadraticAttenuation 
                  * distance * distance);

In any case, the factor attenuation should then be multiplied with gl_LightSource[0].diffuse to compute the incoming light; see the complete shader code below. Note that spot light sources have additional features, which are discussed in the next section.

Also note that this code is unlikely to give you the best performance because any if is usually quite costly. Since gl_LightSource[0].position.w is either 0 or 1, it is actually not too hard to rewrite the code to avoid the use of if and optimize a bit further:

            vec3 vertexToLightSource = vec3(gl_LightSource[0].position 
               - gl_ModelViewMatrix * gl_Vertex 
               * gl_LightSource[0].position.w);
            float one_over_distance = 
               1.0 / length(vertexToLightSource);
            float attenuation = mix(1.0, one_over_distance, 
               gl_LightSource[0].position.w); 
            vec3 lightDirection = 
               vertexToLightSource * one_over_distance;

However, we will use the version with if for clarity. (“Keep it simple, stupid!”)

Changes for a Spotlight edit

The 0th light source is a spotlight only if gl_LightSource[0].spotCutoff is less than or equal to 90.0 (otherwise it should be 180.0). Thus, the test could be:

            if (gl_LightSource[0].spotCutoff <= 90.0) // spotlight?

The shape of a spotlight is described by the built-in uniforms gl_LightSource[0].spotDirection, gl_LightSource[0].spotExponent and gl_LightSource[0].spotCutoff. Specifically, if the cosine of the angle between -lightDirection and gl_LightSource[0].spotDirection is smaller than the cosine of gl_LightSource[0].spotCutoff, i.e. if the shaded point is outside the cone of light around the spotlight direction, then the attenuation factor is set to  . We can compute the cosine of the angle between the two vectors by a dot product of the two normalized vectors. The cosine of gl_LightSource[0].spotCutoff is actually provided in gl_LightSource[0].spotCosCutoff. If we include clamping of the cosine at 0 in order to ignore points behind the spotlight, the test is:

                  float clampedCosine = max(0.0, dot(-lightDirection, 
                     gl_LightSource[0].spotDirection));
                  if (clampedCosine < gl_LightSource[0].spotCosCutoff) 
                     // outside of spotlight cone?
                  {
                     attenuation = 0.0;
                  }

Otherwise, if a point is inside the cone of light, the attenuation in OpenGL is supposed to be computed this way:

                     attenuation = attenuation * pow(clampedCosine, 
                        gl_LightSource[0].spotExponent);

This allows for wider (smaller spotExponent) and narrower (larger spotExponent) spotlights.

However, Blender's built-in shader appear to do something more similar to:

                     attenuation = attenuation * pow(clampedCosine
                        - gl_LightSource[0].spotCosCutoff, 
                        gl_LightSource[0].spotExponent / 128.0);

We will stick to the OpenGL version here.

Complete Shader Code edit

All in all, our new vertex shader for a single directional light, point light, or spotlight with linear attenuation becomes:

         varying vec4 color; 
 
         void main()
         {                              
            vec3 normalDirection = 
               normalize(gl_NormalMatrix * gl_Normal);
            vec3 lightDirection;
            float attenuation;
 
            if (0.0 == gl_LightSource[0].position.w) 
               // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = 
                  normalize(vec3(gl_LightSource[0].position));
            } 
            else // point light or spotlight (or other kind of light) 
            {
               vec3 vertexToLightSource = 
                  vec3(gl_LightSource[0].position 
                  - gl_ModelViewMatrix * gl_Vertex);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
 
               if (gl_LightSource[0].spotCutoff <= 90.0) // spotlight?
               {
                  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[0].diffuse) 
               * vec3(gl_FrontMaterial.emission)
               * max(0.0, dot(normalDirection, lightDirection));
 
            color = vec4(diffuseReflection, 1.0);
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }

And the fragment shader is still:

         varying vec4 color;

         void main()
         {            
            gl_FragColor = color;
         }

Summary edit

Congratulations! You already learned a lot about OpenGL lights. This is essential for the following tutorials about more advanced lighting. Specifically, we have seen:

  • What diffuse reflection is and how to describe it mathematically.
  • How to implement diffuse reflection for a single directional light source in a shader.
  • How to extend the shader for point light sources with a linear attenuation.
  • How to further extend the shader to handle spotlights.

Further Reading edit

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.