Shader Series 3: Per-Pixel Lighting Sample

This sample explains how to move lighting calculations to the pixel shader for high-quality per-pixel lighting. Additionally, this sample uses Phong reflection to approximate specular light, creating "highlights" on the object. Specular highlights are light that is reflected directly to a viewer and can be simulated through a number of techniques.

Note
  • This is the third sample in the Shader Series. Before you begin, you should be familiar with the content of the previous entries in the series, as well as the Shader Primer article.
  • The code for this sample is similar to the VertexLighting Sample. Refer to the VertexLighting Sample and associated document for more information about this code.
  • The models presented in this sample are the same used in the VertexLighting Sample.

Sample Controls

This sample uses the following keyboard and gamepad controls.

Action Keyboard control Gamepad control
Rotate the camera W, A, S, and D Right analog D-pad
Rotate the mesh UP ARROW, DOWN ARROW, LEFT ARROW, and RIGHT ARROW Left analog D-pad
Zoom in Z A
Zoom out X B
Use a different technique to render the current 3D primitive SPACEBAR Y
Display a different 3D primitive TAB X
Change the specular power constant for the material * or / D-pad Left/Right
Change the specular intensity constant for the material + or - D-pad Up/Down
Exit the sample ESC or ALT+F4 BACK

Separating the Light Components

In the classic fixed-function graphics pipeline, the color of a primitive via illumination is a function called the "Phong Reflectance Model." This function can be written as:

Total Light = Ambient + Diffuse + Specular

The Phong model also provides definitions for each of these components.

  • Diffuse (also called Lambertian) is the easiest to understand. It is the light reflected by the object from a directional light source. Diffuse light is easy to calculate from the light direction and the normal vector of the primitive at a given vertex or pixel. The VertexLighting Sample (Shader Series 1) demonstrated how to compute diffuse lighting in the vertex shader.
  • Ambient is an approximation of scattered light in the scene. Scattered light comes from the real-world property of diffuse interflection. The light that leaves the surface of an object and enters your eye also bounces in all directions. Some of this scattered light will be reflected off of other objects toward the viewer. In this model, however, the scattered light is not directly modeled; instead, ambient light is added to all vertices or pixels in the scene uniformly. While this is not mathematically accurate, it adds some color to vertices or pixels that do not receive any diffuse light, which is typically more realistic than no illumination at all.
  • Specular is a component of the Phong model that may be unfamiliar. Specular reflectance can be thought of as "mirror-like" reflections, like the highlights you see in shiny substances such as glass and metal. This component gives the viewer a better idea of the kind of material being rendered.

The reality is that light does not actually separate nicely into these components and actually combines properties of all three into the light that you see reflected from real-world materials. Shader programs give you the flexibility to add as much or as little accuracy as you would like. It also allows you to tailor your lighting model to suit your 3D application’s visual style and performance requirements.

Specular Component

The specular component is dependent on a new variable—the viewer's aspect—the angle between the direction of the viewer and the surface. Specular light changes on a surface as a viewer's aspect to that surface changes. This is a key visual "hint" that a viewer is looking at something with specific reflective properties, particularly as the viewer rotates the camera. The diffuse-only versions of the Effects do not give this additional perspective when just moving the camera.

When calculating diffuse light, we consider only a surface normal when determining the contribution of light to a surface. For view-dependent lighting, we need to relate that in some way to the viewer's aspect. To do so, Phong illumination considers how a light is reflected off of a surface. Let's suppose a ray of light strikes a point on a surface and reflects perfectly from that surface. The direction of reflection is known as a reflection vector.

This reflection vector can be calculated with the following function.

vReflection = 2.0 * vNormal * ( vNormal • vLight ) − vLight

The proof of this equation is beyond the scope of this sample. Fortunately, HLSL provides a reflect() intrinsic function that does the same operation (see example 1.6 in VertexLighting.fx).

The next step for Phong reflection is to relate the reflection vector to the view direction. Again, we use a dot product to determine the contribution of light at the point. Like diffuse lighting, a dot product is a convenient way of calculating fall-off by giving an angle between two vectors. For this part of the specular equation, we calculate the angle between the direction to the viewer and the light reflection to determine the intensity of the specular light.

The last component of specular light in the Phong model is a quantification of the "shininess" of a given material. This is done with a pair of constants known as specular power and specular intensity. The dot product of the reflection vector and the view direction vector are taken to the power of the specular power and multiplied by the specular intensity. The resulting equation looks like this:

Specular = specularIntensity * (( vReflection • vViewDirection ) ^ specularPower)

You can combine this value with the ambient and diffuse lighting values to complete the Phong equation.

Per-Pixel Lighting

You may notice upon trying the sample for the first time that the per-vertex calculation for specular lighting doesn't look very good. This is because the lighting equation is evaluated at each vertex, and interpolated across each triangle via the Color semantic (as output from the vertex shader and input to the pixel shader). This interpolation does not necessarily match the value we would have at each individual pixel, particularly with respect to the specular component.

We can achieve a much smoother visual effect by evaluating the specular lighting component at each pixel. Recalculating Phong illumination at each pixel is a technique known as Phong shading and is a great starting point for more elaborate lighting models. The sample also includes shaders that move the diffuse calculation to the pixel shader. While the benefit from computing diffuse lighting per-pixel is less dramatic than that of specular, there are fewer artifacts in the diffuse light.

Implementation of Per-Vertex Phong Reflection

The first thing to notice in VertexLighting.fx is that it is fairly similar to the effect presented in Shader Series 1: VertexLighting Sample. There are, however, a few new constants. Example 1.1 declares the specular power and specular intensity of the material being rendered. Example 1.2 is a declaration of a camera position that also figures into the specular function. As always, these parameters are set by the sample application through EffectParameters in the C# source code.

Example 1.3 shows a step required for many kinds of shader operations. The single light in the scene is represented in world-space coordinates. For the lighting equations to make sense, all of the other vector components must also be transformed to world space. This applies whether working in model space, world space, view space, or projection space.

Example 1.4 is another common, important step: ensuring the coordinate is in homogenous space. This is typically necessary only when transforming positions, and is not necessary for the direction vectors in this effect.

The first application of the previous discussion of specular highlighting can be found in Example 1.6. This section is the vertex shader implementation of the equation for determining a reflection vector from a point light source. Example 1.7 completes the Phong specular equation by taking the dot product of the view direction (the direction to the camera) and the reflection vector. Specular power and intensity are applied to give an overall specular contribution. This is multiplied by the color of the specular light to give the total specular color for the vertex. Note that the dot product is clamped from 0 to 1 by using the saturate intrinsic. This is done because, like diffuse intensity, negative values do not make sense, so they are omitted.

Finally, in Example 1.8, the Phong reflection equation is completed by summing the ambient, diffuse, and specular components and storing them in the vertex color register. These colors are then interpolated into screen-space pixel values. The simplistic pixel shader simply returns the interpolated color.

Implementation of Per-Pixel Phong Reflection

In Shader Series 2, TEXCOORD outputs were used to send interpolated texture coordinates to the pixel shader for use in looking up texture values at each pixel. It turns out that this linear interpolation makes the TEXCOORD registers far more flexible than their name suggests. These registers are used to send anything that can be sensibly linearly interpolated to pixels from the vertex shader.

This is a tremendously powerful technique, and it will appear in all but the most basic shaders. Using the texcoord registers to store additional data about a pixel enables a wide range of data to be made available when the pixel shader is run on that pixel. In PerPixelLighting.fx, Example 2.1, the vertex shader output structure uses TEXCOORD registers 0 and 1 to store world normals and positions, respectively. The pixel shader inputs correspond to these texcoord registers and are named appropriately.

The execution of this usage pattern can be seen in Example 2.2. The world normal and world position are set as outputs so they can be consumed at a per-pixel level in the pixel shader. Diffuse light is being calculated in the vertex shader and is added to the ambient component in Example 2.3.

The pixel shader code in Example 2.4 is nearly identical to the per-vertex implementation of Phong specular, but it is being done with much higher granularity at the pixel level. Using the interpolated world positions and normals, calculating the lighting effect at each pixel gives a much more precise approximation of specular light. In Example 2.5, the specular component is added to the input color, which is the Gouraud shaded diffuse plus ambient components from the vertex shader.

Starting with Example 2.6, some other per-pixel vertex and pixel shaders are introduced that move the entire lighting calculation to the pixel shader. The vertex shader simply acts as a setup step to create the proper inputs for the pixel shaders in Examples 2.8 and 2.9. Calculating diffuse light with per-pixel granularity gives less dramatic results than that of specular lighting, but there is a small image improvement.

The drawback to moving calculations to the pixel shader is that there are far more pixels in our scene than vertices. Therefore, pixel-shader operations will be run more times than those in a vertex shader, and this solution may run slower than the per-vertex techniques. It is up to the developer to determine the performance versus quality tradeoff when developing lighting shaders.

The BasicEffect class built into the framework implements a Phong lighting model with ambient, diffuse, and specular components by using the same computations described in this sample. By default, BasicEffect uses per-vertex lighting, but you can set the BasicEffect.PreferPerPixelLighting property to true to use per-pixel lighting instead.

Extending the Sample

Specular Textures: In Shader Series 2: Textures and Colors Sample, the concept of pixel-shader texturing was introduced. Armed with the ability to apply specular lighting, you can now create specular textures (also called specular maps) to add another dimension of detail to the geometry in the scene. Combining diffuse and specular textures can create very realistic lighting effects. It can also be applied to create fantastic or other-worldly lighting effects to represent non-realistic materials.

Blinn-Phong Reflection: A potentially more efficient and more physically accurate variation of Phong illumination is Blinn-Phong reflection. Try researching the Blinn-Phong shading model and modifying the sample to use Blinn-Phong. Hint: instead of passing a world position and world normal via texcooord registers, try passing the "half-vector" and the world normal.