Last modified on 19 May 2012, at 19:57

GLSL Programming/GLUT/Two-Sided Surfaces

An algebraic surface that includes an oloid in the center. The rendering uses different colors for the two sides of the surface.

This tutorial covers two-sided per-vertex lighting.

It's part of a series of tutorials about basic lighting in OpenGL. In this tutorial, we extend the tutorial on specular highlights to render two-sided surfaces. If you haven't read the tutorial on specular highlights, this would be a very good time to read it.

The surface normal vector N and the direction to the viewer V are usually on the same side of the surface but there are exceptions; thus, one shouldn't rely on it.

Two-Sided LightingEdit

As shown by the figure to the left, it's sometimes useful to apply different colors to the two sides of a surface. In the tutorial on cutaways, we have seen how a fragment shader can use the built-in variable gl_FrontFacing to determine whether a fragment is part of a front-facing or a back-facing triangle. Can a vertex shader also determine whether it is part of a front-facing or a back-facing triangle? The answer is a clear: no! One reason is that the same vertex can be part of a front-facing and a back-facing triangle at the same time; thus, whatever decision is made in the vertex shader, it is potentially wrong for some triangles. If you want a simple rule to remember: “Fragments are either front-facing or back-facing. Vertices are bi.”

Thus, two-sided per-vertex lighting has to let the fragment shader determine, whether the front or the back material color should be applied. For example, with this fragment shader:

varying vec4 frontColor; // color for front face
varying vec4 backColor; // color for back face
 
void main()
{
  if (gl_FrontFacing) // is the fragment part of a front face?
    {
      gl_FragColor = frontColor;
    }
  else // fragment is part of a back face
    {
      gl_FragColor = backColor;
    }
}

On the other hand, this means that the vertex shader has to compute the surface lighting twice: for a front face and for a back face. Fortunately, this is usually still less work than computing the surface lighting for each fragment.

Vertex Shader CodeEdit

The vertex shader code for two-sided per-vertex lighting is a straightforward extension of the code in the tutorial on specular highlights. However, we have to deactivate back-face culling as described in the tutorial on transparency. Furthermore, the shader requires two sets of material parameters (front and back), which could be available as gl_FrontMaterial and gl_BackMaterial. With these two sets of parameters, the vertex shader could compute two colors, one for front faces and one for back faces, where the negated normal vector has to be used for the back faces. Both colors are handed to the fragment shader, which decides which color to apply. The vertex shader code is then:

attribute vec4 v_coord;
attribute vec3 v_normal;
uniform mat4 m, v, p;
uniform mat3 m_3x3_inv_transp;
uniform mat4 v_inv;
varying vec4 frontColor; // color for front face
varying vec4 backColor; // color for back face
 
struct lightSource
{
  vec4 position;
  vec4 diffuse;
  vec4 specular;
  float constantAttenuation, linearAttenuation, quadraticAttenuation;
  float spotCutoff, spotExponent;
  vec3 spotDirection;
};
lightSource light0 = lightSource(
  vec4(0.0,  1.0,  2.0, 1.0),
  vec4(1.0,  1.0,  1.0, 1.0),
  vec4(1.0,  1.0,  1.0, 1.0),
  0.0, 1.0, 0.0,
  180.0, 0.0,
  vec3(0.0, 0.0, 0.0)
);
vec4 scene_ambient = vec4(0.2, 0.2, 0.2, 1.0);
 
struct material
{
  vec4 ambient;
  vec4 diffuse;
  vec4 specular;
  float shininess;
};
material frontMaterial = material(
  vec4(0.2, 0.2, 0.2, 1.0),
  vec4(1.0, 0.8, 0.8, 1.0),
  vec4(1.0, 1.0, 1.0, 1.0),
  5.0
);
material backMaterial = material(
  vec4(0.2, 0.2, 0.2, 1.0),
  vec4(0.0, 0.0, 1.0, 1.0),
  vec4(1.0, 1.0, 1.0, 1.0),
  5.0
);
 
void main(void)
{
  mat4 mvp = p*v*m;
  vec3 normalDirection = normalize(m_3x3_inv_transp * v_normal);
  vec3 viewDirection = normalize(vec3(v_inv * vec4(0.0, 0.0, 0.0, 1.0) - m * v_coord));
  vec3 lightDirection;
  float attenuation;
 
  if (light0.position.w == 0.0) // directional light
    {
      attenuation = 1.0; // no attenuation
      lightDirection = normalize(vec3(light0.position));
    }
  else // point or spot light (or other kind of light)
    {
      vec3 vertexToLightSource = vec3(light0.position - m * v_coord);
      float distance = length(vertexToLightSource);
      lightDirection = normalize(vertexToLightSource);
      attenuation = 1.0 / (light0.constantAttenuation
			   + light0.linearAttenuation * distance
			   + light0.quadraticAttenuation * distance * distance);
 
      if (light0.spotCutoff <= 90.0) // spotlight
	{
	  float clampedCosine = max(0.0, dot(-lightDirection, normalize(light0.spotDirection)));
	  if (clampedCosine < cos(radians(light0.spotCutoff))) // outside of spotlight cone
	    {
	      attenuation = 0.0;
	    }
	  else
	    {
              attenuation = attenuation * pow(clampedCosine, light0.spotExponent);
	    }
	}
    }
 
  // Computation of lighting for front faces
 
  vec3 ambientLighting = vec3(scene_ambient) * vec3(frontMaterial.ambient);
 
  vec3 diffuseReflection = attenuation
    * vec3(light0.diffuse) * vec3(frontMaterial.diffuse)
    * 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(light0.specular) * vec3(frontMaterial.specular)
	* pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)),
	      frontMaterial.shininess);
    }
 
  frontColor = vec4(ambientLighting + diffuseReflection + specularReflection, 1.0);
 
  // Computation of lighting for back faces (uses negative normalDirection and back material colors)
 
  vec3 backAmbientLighting = vec3(scene_ambient) * vec3(backMaterial.ambient);
 
  vec3 backDiffuseReflection = attenuation
    * vec3(light0.diffuse) * vec3(backMaterial.diffuse)
    * max(0.0, dot(-normalDirection, lightDirection));
 
  vec3 backSpecularReflection;
  if (dot(-normalDirection, lightDirection) < 0.0) // light source on the wrong side?
    {
      backSpecularReflection = vec3(0.0, 0.0, 0.0); // no specular reflection
    }
  else // light source on the right side
    {
      backSpecularReflection = attenuation * vec3(light0.specular) * vec3(backMaterial.specular)
	* pow(max(0.0, dot(reflect(-lightDirection, -normalDirection), viewDirection)),
	      backMaterial.shininess);
    }
 
  backColor = vec4(backAmbientLighting + backDiffuseReflection + backSpecularReflection, 1.0);
 
  gl_Position = mvp * v_coord;
}

The fragment shader code is described above.

SummaryEdit

Congratulations, you made it to the end of this short tutorial with a long shader. We have seen:

  • Why a vertex shader cannot distinguish between front-facing and back-facing vertices (because the same vertex might be part of a front-facing and a back-facing triangles).
  • How to compute lighting for front faces and for back faces in the vertex shader.
  • How to let the fragment shader decide which color to apply.

Further ReadingEdit

If you still want to know more


< GLSL Programming/GLUT

Unless stated otherwise, all example source code on this page is granted to the public domain.
Back to OpenGL Programming - Lighting section Back to GLSL Programming - GLUT section