18 Lights and Shadows

18.010 What should I know about lighting in general?

You must specify normals along with your geometry, or you must generate them automatically with evaluators, in order for lighting to work as expected. This is covered in question 18.020.

Lighting does not work with the current color as set by glColor*(). It works with material colors. Set the material colors with glMaterial*(). Material colors can be made to track the current color with the color material feature. To use color material, call glEnable(GL_COLOR_MATERIAL). By default, this causes ambient and diffuse material colors to track the current color. You can specify which material color tracks the current color with a call to glColorMaterial().

Changing the material colors with color material and glColor*() calls may be more efficient than using glMaterial*(). See question 18.080 for more information.

Lighting is computed at each vertex (and interpolated across the primitive, when glShadeModel() is set to GL_SMOOTH). This may cause primitives to appear too dark, even though a light is centered over the primitive. You can obtain more correct lighting with a higher surface approximation, or by using light maps.

A light's position is transformed by the current ModelView matrix at the time the position is specified with a call to glLight*(). This is analogous to how geometric vertices are transformed by the current ModelView matrix when they are specified with a call to glVertex*(). For more information on positioning your light source, see question 18.050.

18.020 Why are my objects all one flat color and not shaded and illuminated?

This effect occurs when you fail to supply a normal at each vertex.

OpenGL needs normals to calculate lighting equations, and it won't calculate normals for you (with the exception of evaluators). If your application doesn't call glNormal*(), then it uses the default normal of (0.0, 0.0, 1.0) at every vertex. OpenGL will then compute the same, or nearly the same, lighting result at each vertex. This will cause your model to look flat and lack shading.

The solution is to simply calculate the normals that need to be specified at any given vertex. Then send them to OpenGL with a call to glNormal3f() just prior to specifying the vertex, which the normal is associated with.

If you don't know how to calculate a normal, in most cases you can do it simply with a vector cross product. The OpenGL Programming Guide contains a small section explaining how to calculate normals. Also most basic 3D computer graphics books cover it, because it's not OpenGL-specific.

18.030 How can I make OpenGL automatically calculate surface normals?

OpenGL won't do this unless you're using evaluators.

18.040 Why do I get only flat shading when I light my model?

First, check the obvious. glShadeModel() should be set to GL_SMOOTH, which is the default value, so if you haven't called glShadeModel() at all, it's probably already set to GL_SMOOTH, and something else is wrong.

If glShadeModel() is set correctly, the problem is probably with your surface normals. To achieve a smooth shading effect, generally you need to specify a different normal at each vertex. If you have set the same normal at each vertex, the result, in most cases, will be a flatly shaded primitive.

Keep in mind that a typical surface normal is perpendicular to the surface that you're attempting to approximate.

This scenario can be tough to debug, especially for large models. The best debugging approach is to write a small test program that draws only one primitive, and try to reproduce the problem. It's usually easy to use a debugger to isolate and fix a small program, which reproduces the problem.

18.050 How can I make my light move or not move and control the light position?

First, you must understand how the light position is transformed by OpenGL.

The light position is transformed by the contents of the current top of the ModelView matrix stack when you specify the light position with a call to glLightfv(GL_LIGHT_POSITION,…). If you later change the ModelView matrix, such as when the view changes for the next frame, the light position isn't automatically retransformed by the new contents of the ModelView matrix. If you want to update the light’s position, you must again specify the light position with a call to glLightfv(GL_LIGHT_POSITION,…).

Asking the question “how do I make my light move” or “how do I make my light stay still” usually doesn't provide enough information to answer the question. For a better answer, you need to be more specific. Here are some more specific questions, and their answers:

  • How can I make my light position stay fixed relative to my eye position? How do I make a headlight?

You need to specify your light in eye coordinate space. To do so, set the ModelView matrix to the identity, then specify your light position. To make a headlight (a light that appears to be positioned at or near the eye and shining along the line of sight), set the ModelView to the identity, set the light position at (or near) the origin, and set the direction to the negative Z axis.

When a light’s position is fixed relative to the eye, you don't need to respecify the light position for every frame. Typically, you specify it once when your program initializes.

  • How can I make my light stay fixed relative to my scene? How can I put a light in the corner and make it stay there while I change my view?

As your view changes, your ModelView matrix also changes. This means you'll need to respecify the light position, usually at the start of every frame. A typical application will display a frame with the following pseudocode:

Set the view transform.
Set the light position //glLightfv(GL_LIGHT_POSITION,…)
Send down the scene or model geometry.
Swap buffers.

If your light source is part of a light fixture, you also may need to specify a modeling transform, so the light position is in the same location as the surrounding fixture geometry.

  • How can I make a light that moves around in a scene?

Again, you'll need to respecify this light position every time the view changes. Additionally, this light has a dynamic modeling transform that also needs to be in the ModelView matrix before you specify the light position. In pseudocode, you need to do something like:

Set the view transform
Push the matrix stack
Set the model transform to update the light’s position
Set the light position //glLightfv(GL_LIGHT_POSITION,…)
Pop the matrix stack
Send down the scene or model geometry
Swap buffers.

18.060 How can I make a spotlight work?

A spotlight is simply a point light source with a small light cone radius. Alternatively, a point light is just a spot light with a 180 degree radius light cone. Set the radius of the light cone by changing the cutoff parameter of the light:

glLightf (GL_LIGHT1, GL_SPOT_CUTOFF, 15.f);

The above call sets the light cone radius to 15 degrees for light 1. The light cone's total spread will be 30 degrees.

A spotlight's position and direction are set as for any normal light.

18.070 How can I create more lights than GL_MAX_LIGHTS?

First, make sure you really need more than OpenGL provides. For example, when rendering a street scene at night with many buildings and streetlights, you need to ask yourself: Does every building need to be illuminated by every single streetlight? When light attenuation and direction are accounted for, you may find that any given piece of geometry in your scene is only illuminated by a small handful of lights.

If this is the case, you need to reuse or cycle the available OpenGL lights as you render your scene.

The GLUT distribution comes with a small example that might be informative to you. It’s called multilight.c.

If you really need to have a single piece of geometry lit by more lights than OpenGL provides, you'll need to simulate the effect somehow. One way is to calculate the lighting for some or all the lights. Another method is to use texture maps to simulate lighting effects.

18.080 Which is faster: making glMaterial*() calls or using glColorMaterial()?

Within a glBegin()/glEnd() pair, on most OpenGL implementations, a call to glColor3f() generally is faster than a call to glMaterialfv(). This is simply because most implementations tune glColor3f(), and processing a material change can be complex and difficult to optimize. For this reason, glColorMaterial() generally is recognized as the most efficient way to change an object’s material color.

18.090 Why is the lighting incorrect after I scale my scene to change its size?

The OpenGL specification needs normals to be unit length to achieve typical lighting results. The current ModelView matrix transforms normals. If that matrix contains a scale transformation, transformed normals might not be unit length, resulting in undesirable lighting problems.

OpenGL 1.1 lets you call glEnable(GL_NORMALIZE), which will make all normals unit length after they're transformed. This is often implemented with a square root and can be expensive for geometry limited applications.

Another solution, available in OpenGL 1.2 (and as an extension to many 1.1 implementations), is glEnable(GL_RESCALE_NORMAL). Rather than making normals unit length by computing a square root, GL_RESCALE_NORMAL multiplies the transformed normal by a scale factor. If the original normals are unit length, and the ModelView matrix contains uniform scaling, this multiplication will restore the normals to unit length.

If the ModelView matrix contains nonuniform scaling, GL_NORMALIZE is the preferred solution.

18.100 After I turn on lighting, everything is lit. How can I light only some of the objects?

Remember that OpenGL is a state machine. You'll need to do something like this:

glEnable(GL_LIGHTING);
// Render lit geometry.
glDisable(GL_LIGHTING);
// Render non-lit geometry.

18.110 How can I use light maps (e.g., Quake-style) in OpenGL?

See this question in the Texture Mapping section.

18.120 How can I achieve a refraction lighting effect?

First, consider whether OpenGL is the right API for you. You might need to use a ray tracer to achieve complex light affects such as refraction.

If you're certain that you want to use OpenGL, you need to keep in mind that OpenGL doesn’t provide functionality to produce a refraction effect. You'll need to fake it. The most likely solution is to calculate an image corresponding to the refracted rendering, and texture map it onto the surface of the primitive that's refracting the light.

18.130 How can I render caustics?

OpenGL can't help you render caustics, except for texture mapping. GLUT 3.7 comes with some demos that show you how to achieve caustic lighting effects.

18.140 How can I add shadows to my scene?

The GLUT 3.7 distribution comes with examples that demonstrate how to do this using projection shadows and the stencil buffer.

Projection shadows are ideal if your shadow is only to lie on a planar object. You can generate geometry of the shadow using glFrustum() to transform the object onto the projection plane.

Stencil buffer shadowing is more flexible, allowing shadows to lie on any object, planar or otherwise. The basic algorithm is to calculate a "shadow volume". Cull the back faces of the shadow volume and render the front faces into the stencil buffer, inverting the stencil values. Then render the shadow volume a second time, culling front faces and rendering the back faces into the stencil buffer, again inverting the stencil value. The result is that the stencil planes will now contain non-zero values where the shadow should be rendered. Render the scene a second time with only ambient light enabled and glDepthFunc() set to GL_EQUAL. The result is a rendered shadow.

Another mechanism for rendering shadows is outlined in the SIGGRAPH '92 paper Fast Shadows and Lighting Effects Using Texture Mapping, Mark Segal et al. This paper describes a relatively simple extension to OpenGL for using the depth buffer as a shadow texture map. Both the GL_EXT_depth_texture and the GL_EXT_texture3D (or OpenGL 1.2) extensions are required to use this method.