- OpenGL 4 Shading Language Cookbook(Second Edition)
- David Wolff
- 2056字
- 2021-07-21 17:51:00
Implementing per-vertex ambient, diffuse, and specular (ADS) shading
The OpenGL fixed function pipeline implemented a default shading technique which is very similar to the one presented here. It models the light-surface interaction as a combination of three components: ambient, diffuse, and specular. The ambient component is intended to model light that has been reflected so many times that it appears to be emanating uniformly from all directions. The diffuse component was discussed in the previous recipe, and represents omnidirectional reflection. The specular component models the shininess of the surface and represents reflection around a preferred direction. Combining these three components together can model a nice (but limited) variety of surface types. This shading model is also sometimes called the Phong reflection model (or Phong shading model), after Bui Tuong Phong.
An example of a torus rendered with the ADS shading model is shown in the following screenshot:
The ADS model is implemented as the sum of the three components: ambient, diffuse, and specular. The ambient component represents light that illuminates all surfaces equally and reflects equally in all directions. It is often used to help brighten some of the darker areas within a scene. Since it does not depend on the incoming or outgoing directions of the light, it can be modeled simply by multiplying the light source intensity (La) by the surface reflectivity (Ka).
The diffuse component models a rough surface that scatters light in all directions (refer to the Implementing diffuse, per-vertex shading with a single point light source recipe in this chapter). The intensity of the outgoing light depends on the angle between the surface normal and the vector towards the light source.
The specular component is used for modeling the shininess of a surface. When a surface has a glossy shine to it, the light is reflected off of the surface in a mirror-like fashion. The reflected light is strongest in the direction of perfect (mirror-like) reflection. The physics of the situation tells us that for perfect reflection, the angle of incidence is the same as the angle of reflection and that the vectors are coplanar with the surface normal, as shown in the following diagram:
In the preceding diagram, r represents the vector of pure-reflection corresponding to the incoming light vector (-s), and n is the surface normal. We can compute r by using the following equation:
To model specular reflection, we need to compute the following (normalized) vectors: the direction towards the light source (s), the vector of perfect reflection (r), the vector towards the viewer (v), and the surface normal (n). These vectors are represented in the following diagram:
We would like the reflection to be maximal when the viewer is aligned with the vector r, and to fall off quickly as the viewer moves further away from alignment with r. This can be modeled using the cosine of the angle between v and r raised to some power (f).
(Recall that the dot product is proportional to the cosine of the angle between the vectors involved.) The larger the power, the faster the value drops towards zero as the angle between v and r increases. Again, similar to the other components, we also introduce a specular light intensity term (Ls) and reflectivity term (Ks).
The specular component creates specular highlights (bright spots) that are typical of glossy surfaces. The larger the power of f in the equation, the smaller the specular highlight and the shinier the surface appears. The value for f is typically chosen to be somewhere between 1 and 200.
Putting all of this together, we have the following shading equation:
For more details about how this shading model was implemented in the fixed function pipeline, take a look at Chapter 5, Image Processing and Screen Space Techniques.
In the following code, we'll evaluate this equation in the vertex shader, and interpolate the color across the polygon.
Getting ready
In the OpenGL application, provide the vertex position in location 0 and the vertex normal in location 1. The light position and the other configurable terms for our lighting equation are uniform variables in the vertex shader and their values must be set from the OpenGL application.
How to do it...
To create a shader pair that implements ADS shading, use the following steps:
- Use the following code for the vertex shader:
layout (location = 0) in vec3 VertexPosition; layout (location = 1) in vec3 VertexNormal; out vec3 LightIntensity; struct LightInfo { vec4 Position; // Light position in eye coords. vec3 La; // Ambient light intensity vec3 Ld; // Diffuse light intensity vec3 Ls; // Specular light intensity }; uniform LightInfo Light; struct MaterialInfo { vec3 Ka; // Ambient reflectivity vec3 Kd; // Diffuse reflectivity vec3 Ks; // Specular reflectivity float Shininess; // Specular shininess factor }; uniform MaterialInfo Material; uniform mat4 ModelViewMatrix; uniform mat3 NormalMatrix; uniform mat4 ProjectionMatrix; uniform mat4 MVP; void main() { vec3 tnorm = normalize( NormalMatrix * VertexNormal); vec4 eyeCoords = ModelViewMatrix * vec4(VertexPosition,1.0); vec3 s = normalize(vec3(Light.Position - eyeCoords)); vec3 v = normalize(-eyeCoords.xyz); vec3 r = reflect( -s, tnorm ); vec3 ambient = Light.La * Material.Ka; float sDotN = max( dot(s,tnorm), 0.0 ); vec3 diffuse = Light.Ld * Material.Kd * sDotN; vec3 spec = vec3(0.0); if( sDotN > 0.0 ) spec = Light.Ls * Material.Ks * pow(max( dot(r,v), 0.0 ), Material.Shininess); LightIntensity = ambient + diffuse + spec; gl_Position = MVP * vec4(VertexPosition,1.0); }
- Use the following code for the fragment shader:
in vec3 LightIntensity; layout( location = 0 ) out vec4 FragColor; void main() { FragColor = vec4(LightIntensity, 1.0); }
- Compile and link both shaders within the OpenGL application, and install the shader program prior to rendering.
How it works...
The vertex shader computes the shading equation in eye coordinates. It begins by transforming the vertex normal into eye coordinates and normalizing, then storing the result in tnorm
. The vertex position is then transformed into eye coordinates and stored in eyeCoords
.
Next, we compute the normalized direction towards the light source (s
). This is done by subtracting the vertex position in eye coordinates from the light position and normalizing the result.
The direction towards the viewer (v
) is the negation of the position (normalized) because in eye coordinates the viewer is at the origin.
We compute the direction of pure reflection by calling the GLSL built-in function reflect
, which reflects the first argument about the second. We don't need to normalize the result because the two vectors involved are already normalized.
The ambient component is computed and stored in the variable ambient
. The dot product of s
and n
is computed next. As in the preceding recipe, we use the built-in function max
to limit the range of values to between one and zero. The result is stored in the variable named sDotN
, and is used to compute the diffuse component. The resulting value for the diffuse component is stored in the variable diffuse
. Before computing the specular component, we check the value of sDotN
. If sDotN
is zero, then there is no light reaching the surface, so there is no point in computing the specular component, as its value must be zero. Otherwise, if sDotN
is greater than zero, we compute the specular component using the equation presented earlier. Again, we use the built-in function max
to limit the range of values of the dot product to between one and zero, and the function pow
raises the dot product to the power of the Shininess
exponent (corresponding to f in our lighting equation).
Note
If we did not check sDotN
before computing the specular component, it is possible that some specular highlights could appear on faces that are facing away from the light source. This is clearly a non-realistic and undesirable result. Some people solve this problem by multiplying the specular component by the diffuse component, which would decrease the specular component substantially and alter its color. The solution presented here avoids this, at the cost of a branch statement (the if
statement). (Branch statements can have a significant impact on performance.)
The sum of the three components is then stored in the output variable LightIntensity
. This value will be associated with the vertex and passed down the pipeline. Before reaching the fragment shader, its value will be interpolated in a perspective correct manner across the face of the polygon.
Finally, the vertex shader transforms the position into clip coordinates, and assigns the result to the built-in output variable gl_Position
(refer to the Implementing diffuse, per-vertex shading with a single point light source recipe in this chapter).
The fragment shader simply applies the interpolated value of LightIntensity
to the output fragment by storing it in the shader output variable FragColor
.
There's more...
This version of the ADS (Ambient, Diffuse, and Specular) reflection model is by no means optimal. There are several improvements that could be made. For example, the computation of the vector of pure reflection can be avoided via the use of the so-called "halfway vector". This is discussed in the Using the halfway vector for improved performance recipe in Chapter 3, Lighting, Shading, and Optimization.
We can avoid the extra normalization needed to compute the vector towards the viewer (v
), by using a so-called non-local viewer. Instead of computing the direction towards the origin, we simply use the constant vector (0, 0, 1) for all vertices. This is similar to assuming that the viewer is located infinitely far away in the z direction. Of course, it is not accurate, but in practice the visual results are very similar, often visually indistinguishable, saving us normalization.
In the old fixed-function pipeline, the non-local viewer was the default, and could be adjusted (turned on or off) using the function glLightModel
.
Since the shading equation is computed within the vertex shader, we refer to this as per-vertex shading. One of the disadvantages of this is that specular highlights can be warped or lost, due to the fact that the shading equation is not evaluated at each point across the face. For example, a specular highlight that should appear in the middle of a polygon might not appear at all when per-vertex shading is used, because of the fact that the shading equation is only computed at the vertices where the specular component is near zero. In the Using per-fragment shading for improved realism recipe of Chapter 3, Lighting, Shading, and Optimization, we'll look at the changes needed to move the shading computation into the fragment shader, producing more realistic results.
We can also avoid the need to compute a light direction (s
), for each vertex if we assume a directional light. A directional light source is one that can be thought of as located infinitely far away in a given direction. Instead of computing the direction towards the source for each vertex, a constant vector is used, which represents the direction towards the remote light source. We'll look at an example of this in the Shading with a directional light source recipe of Chapter 3, Lighting, Shading, and Optimization.
You might think that this shading model is missing one important component. It doesn't take into account the effect of the distance to the light source. In fact, it is known that the intensity of radiation from a source falls off in proportion to the inverse square of the distance from the source. So why not include this in our model?
It would be fairly simple to do so, however, the visual results are often less than appealing. It tends to exaggerate the distance effects and create unrealistic looking images. Remember, our equation is just an approximation of the physics involved and is not a truly realistic model, so it is not surprising that adding a term based on a strict physical law produces unrealistic results.
In the OpenGL fixed-function pipeline, it was possible to turn on distance attenuation using the glLight
function. If desired, it would be straightforward to add a few uniform variables to our shader to produce the same effect.
See also
- The Shading with a directional light source recipe in Chapter 3, Lighting, Shading, and Optimization
- The Using per-fragment shading for improved realism recipe in Chapter 3, Lighting, Shading, and Optimization
- The Using the halfway vector for improved performance recipe in Chapter 3, Lighting, Shading, and Optimization