GLSL Programming/Blender/Two-Sided Smooth Surfaces

This tutorial covers two-sided per-pixel lighting (i.e. two-sided Phong shading).

Rendering of Cayley's nodal cubic surface using different colors on the two sides of the surface.

Here we combine the per-pixel lighting discussed in the tutorial on smooth specular highlights with the two-sided lighting discussed in the tutorial on two-sided surfaces.

Shader Coder edit

The required changes to the code of the tutorial on smooth specular highlights are: new properties for the back material, new local variables for the material parameters in the fragment shader, which are set either to the front material parameters or the back material parameters according to gl_FrontFacing. Also, the surface normal vector is negated in case a back face is rendered. Furthermore, back-face culling has to be deactivated as described in the tutorial on transparency. It's actually quite straightforward. However, keep in mind that Blender apparently never provides different data in gl_FrontMaterial and gl_BackMaterial; thus, you have to replace the uniforms in gl_BackMaterial by user-specified uniforms (see the tutorial on shading in view space).

The vertex shader could look like this:

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

And the fragment shader could be:

         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);
            vec4 ambientColor;
            vec4 diffuseColor;
            vec4 specularColor;
            float shininess;

            if (gl_FrontFacing)
            {
               ambientColor = gl_FrontMaterial.emission;
               diffuseColor = gl_FrontMaterial.emission;
               specularColor = gl_FrontMaterial.specular;
               shininess = gl_FrontMaterial.shininess;
            }
            else
            {
               ambientColor = gl_BackMaterial.emission;
               diffuseColor = gl_BackMaterial.emission;
               specularColor = gl_BackMaterial.specular;
               shininess = gl_BackMaterial.shininess;
               normalDirection = -normalDirection;
            }

            vec3 viewDirection = -normalize(vec3(position)); 
            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 positionToLightSource = 
                  vec3(gl_LightSource[0].position - position);
               float distance = length(positionToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(positionToLightSource);
 
               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 ambientLighting = vec3(gl_LightModel.ambient) 
               * vec3(ambientColor);
              
            vec3 diffuseReflection = attenuation 
               * vec3(gl_LightSource[0].diffuse) * vec3(diffuseColor)
               * 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[0].specular) 
                  * vec3(specularColor) * pow(max(0.0, 
                  dot(reflect(-lightDirection, normalDirection), 
                  viewDirection)), shininess);
            }

            gl_FragColor = vec4(ambientLighting + diffuseReflection 
               + specularReflection, 1.0);
         }

Summary edit

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

  • How two-sided surfaces can be rendered with per-pixel lighting.

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.