We'll talk about how local lighting calculations are performed in the GPU-side shaders.
We'll consider two kinds of light source to illuminate our surfaces:
For sources at infinity, the direction to the source is constant, so the direction can be provided to the shaders as a uniform (i.e. global) variable.
For point sources, the position of the source is constant, to the position can be provided to the shaders as a uniform variable. For point sources, the shaders have to compute the direction to the source.
Lighting calculations on a fragment involve computing N⋅L and R⋅V.
All of the vectors have to be in the same coordinate system for these calculations to be meaningful.
The calculations are usually performed in the viewpoint coordinate system (VCS):
Nvcs=VMNocswhere M is the object-to-world "model transform" and V is the world-to-viewpoint "viewing transform".
Lvcs=VLwcs
Lighting calculations are usually done in the fragment shader, so the vertex shader must provide the VCS coordinates of each vertex in its output. The GPU will perform smooth interpolation on those VCS vertex positions to get VCS fragment positions.
Shading requires coordination betweent the code on the CPU side (C++ in our case), the code in the vertex shader, and the code in the fragment shader.
Since the GPU must use the modelview transform (called MV, but actually V×M) the CPU-side code must provide MV as a uniform.
The CPU-side code must also provide the MVP transform, as usual, so that the vertex shader can compute gl_Position, which is the vertex position, but in the clipping coordinate system.
If the lighting direction is fixed in the world, the CPU-side code must also provide the world-to-viewpoint transform, V, so that the lighting direction can be moved into the VCS.
Finally, the CPU-side code must provide these constants as uniform variables:
Recall that kD, kS, kA, IE, and n are surface properties, so they might be different when rendering different surfaces.
See 03-shaders/shader.cpp in openGL.zip. (But that code is simpler: It does not pass the constant uniforms listed above and it uses a light direction in the VCS.)
The vertex shader needs to output to the fragment shader:
For purely diffuse shading, a vertex shader is
#version 300 es uniform mat4 MVP; // OCS-to-CCS uniform mat4 MV; // OCS-to-VCS layout (location = 0) in vec3 vertPosition; layout (location = 1) in vec3 vertNormal; flat out mediump vec3 colour; smooth out mediump vec3 normal; void main() { // calc vertex position in CCS gl_Position = MVP * vec4( vertPosition, 1.0f ); // assign (r,g,b) colour colour = 2.0 * vec3( 0.33, 0.42, 0.18 ); // calc normal in VCS // // To do this, apply the non-translational parts of MV to the model // normal. The 0.0 as the fourth component of the normal ensures // that no translational component is added. normal = vec3( MV * vec4( vertNormal, 0.0 ) ); }
See 03-shaders/*.vert in openGL.zip for more examples.
The fragment shader does the actual calculation. In the case of purely diffuse shading, a vertex shader is
#version 300 es uniform mediump vec3 lightDir; // direction *toward* light in VCS flat in mediump vec3 colour; smooth in mediump vec3 normal; out mediump vec4 outputColour; void main() { mediump float NdotL = dot( normalize(normal), lightDir ); if (NdotL < 0.0) // For light behind the surface, there is no illumination NdotL = 0.0; mediump vec3 diffuseColour = NdotL * colour; outputColour = vec4( diffuseColour, 1.0 ); }
See 03-shaders/*.frag in openGL.zip for more examples.