Lighting
You may need to read the articles about 3D and varyings before this one.
Accurate lighting is very expensive to calculate in real time, so lighting in OpenGL typically based on approximations of reality using simplified models. One of the most popular of these models is the Phong lighting model, which has three components: ambient, diffuse, and specular.
Diffuse Lighting
Directional lighting (also called diffuse lighting) is light that comes uniformly from one direction. If the direction that the light is traveling and the direction that a surface is facing are both known, it is possible to calculate the brightness of the light on that surface.
The direction that a surface is facing is represented by a unit vector called a normal. Normals are typically passed to the vertex shader with an attribute, then to the fragment shader with a varying.
#version 300 es
in vec4 a_position;
in vec3 a_normal;
uniform mat4 u_viewProjMat;
uniform mat4 u_worldMat;
uniform mat3 u_normalMat;
out vec3 v_normal;
void main() {
gl_Position = u_viewProjMat * u_worldMat * a_position;
v_normal = u_normalMat * a_normal;
}
Notice that the normals are multiplied by a matrix called u_normalMat
. A normal matrix is the inverse transpose of the rotation portion of a transformation matrix. The upper-left three-by-three portion of a transformation matrix is the part that stores rotation data, so the fourth row and column can be ignored. Inverting and transposing this matrix causes the normals to remain correct even when the shape is scaled. The normals are multiplied by the normal matrix but not the view projection matrix, since the light source shouldn't move when the camera moves.
The brightness of a directional light on a surface is equivalent to the cosine of the angle between that surface's normal and the direction from that surface to the light source, which is in turn equivalent to the dot product of those angles. For directional lighting, the direction from the surface to the light source is the opposite of the direction that the light is traveling.
#version 300 es
precision mediump float;
in vec3 v_normal;
uniform vec3 u_reverseLightDir;
uniform vec4 u_color;
out vec4 outColor;
void main() {
vec3 normal = normalize(v_normal);
float brightness = dot(normal, u_reverseLightDir);
outColor = u_color;
outColor.rgb *= brightness;
}
Notice that since the normal is interpolated when being passed to the fragment shader, it has to be normalized before it can be used.
A light source can be given a color by multiplying the brightness by that color.
float brightness = dot(normal, u_reverseLightDir);
vec3 lightColor = u_lightColor * brightness;
outColor = u_color;
outColor.rgb *= lightColor;
Ambient Lighting
Ambient lighting is lighting that is applied equally to all surfaces regardless of normal. It can be used to set a minimum brighness so that nothing in a scene is too dark.
float brightness = diffuseBrightness + u_ambientBrightness;
outColor = u_color;
outColor.rgb *= brightness;
Point Lighting
Point lighting is when light extends in every direction from a point. To emulate it, pass the direction from the surface to the light source as a varying, then use that direction in place of the reverse light direction in the algorithm for diffuse light.
#version 300 es
in vec4 a_position;
in vec3 a_normal;
uniform mat4 u_viewProjMat;
uniform mat4 u_worldMat;
uniform mat3 u_normalMat;
uniform vec3 u_lightPos;
out vec3 v_normal;
out vec3 v_dirToLight;
void main() {
vec4 worldPos = u_worldMat * a_position
gl_Position = u_viewProjMat * worldPos;
v_normal = u_normalMat * a_normal;
v_dirToLight = u_lightPos - worldPos.xyz;
}
Point lighting is almost identical to directional lighting, except that the light direction is interpolated between fragments. Note that, while the reverse light direction is a direction and should thus be normalized, the light position is a position and thus should not be normalized.
Specular Lighting
Specular lighting is when light reflects off of a shiny surface.
Light reflects off of a surface at the same angle that it hits that surface, so specular lighting should be visible if the angle from the light to the surface is similar to the angle from the surface to the camera. Both angles can be calculated by taking the dot product of their vectors, then they can be added together and normalized to get the half vector, which is the vector that sits halfway between them. The brightness of specular light can be determined based on how similar the half vector is to the surface's normal.
First, the direction from the surface to the light and from the surface to the camera must be passed as varyings (using the same process as with point lighting). Then, the brightness of the specular lighting can be calculated as the dot product between the half angle and the normal. The object can be given a "dullness" ("shininess") value to change the specular lighting from a linear to an exponential falloff, which makes it look more realistic.
#version 300 es
precision mediump float;
in vec3 v_normal;
in vec3 v_dirToLight;
in vec3 v_dirToCam;
uniform vec3 u_reverseLightDir;
uniform vec4 u_color;
uniform float u_dullness;
uniform float u_ambientBrightness;
out vec4 outColor;
void main() {
vec3 normal = normalize(v_normal);
vec3 dirToLight = normalize(v_dirToLight);
vec3 dirToCamera = normalize(v_dirToCam);
vec3 halfVector = normalize(dirToLight + dirToCam);
float diffuseBrightness = dot(normal, u_reverseLightDir);
float specularBrightness =
pow(max(dot(normal, halfVector), 0.0), u_dullness);
float brightness = diffuseBrightness
+ specularBrightness
+ u_ambientBrightness;
outColor = u_color;
outColor.rgb *= brightness;
}
Spot Lighting
Spot lighting is the same as point lighting except that the light only extends in a cone. To emulate spot lighting, the cone needs a direction and a limit, which is the angle from the direction of the cone to its sides. If the dot product of the direction to the normal is above the cosine of the limit (the dot limit), the surface is within the cone.
The cone can be given an outer limit and an inner limit between which the brightness values are interpolated, which would give the spotlight a smooth edge.
#version 300 es
precision mediump float;
in vec3 v_normal;
in vec3 v_dirToLight;
uniform vec4 u_color;
uniform vec3 u_reverseLightDir;
uniform float u_innerLimit;
uniform float u_outerLimit;
out vec4 outColor;
void main() {
vec3 normal = normalize(v_normal);
vec3 dirToLight = normalize(v_dirToLight);
float angularDist = dot(dirToLight, u_reverseLightDir);
float brightness = smoothstep(u_outerLimit, u_innerLimit, angularDist)
* dot(normal, dirToLight);
outColor = u_color;
outColor.rgb *= brightness;
}
The next article is about cubemaps.