Lighting

You may need to read the previous articles about 3D and varyings in order to understand this one.

Directional lighting

Directional lighting (or diffuse lighting) is the simplest form of lighting. It assumes that the light is coming uniformly from one direction. One common use case of directional lighting is to emulate light from the sun.

If the direction that the light is traveling and the direction that a surface is facing are both known, it is possible to determine the cosine of the angle between the two directions by taking the dot product. That value represents the "brightness" of the light on that surface.

The direction that a surface is facing is represented by a unit vector called a normal.

#version 300 es

in vec4 a_position;
in vec3 a_normal;

uniform mat4 u_matrix;

out vec3 v_normal;

void main() {
	gl_Position = u_matrix * a_position;
	v_normal = a_normal;
}
#version 300 es

precision highp float;

in vec3 v_normal;

uniform vec4 u_color;

/*
Instead of taking the direction that the light is moving as
input, it is easier to take the position of the light
source (in other words, the direction to the light source).
*/
uniform vec3 u_reverseLightDirection;

out vec4 outColor;

void main() {
	/*
	Since v_normal is a varying, its value is interpolated.
	Therefore, it must be re-normalized to a unit vector.
	*/
	vec3 normal = normalize(v_normal);

	float light = dot(normal, u_reverseLightDirection);

	outColor = u_color;
	outColor.rgb *= light;
}

Notice that although sides of the cube above are being lit properly, the light isn't changing when the cube rotates. This is because the normals aren't being re-oriented like the positions are. Multiplying the normals by the object's world matrix will have the desired effect. Note that this requires the world matrix and the view projection matrix to be passed separately.

#version 300 es

in vec4 a_position;
in vec3 a_normal;

uniform mat4 u_viewProjMat;
uniform mat4 u_worldMat;

out vec3 v_normal;

void main() {
	mat4 mat = u_viewProjMat * u_worldMat;

	gl_Position = mat * a_position;

	/*
	The upper 3x3 of a transformation matrix is the part
	that deals with rotation.
	*/
	v_normal = mat3(u_worldMat) * a_normal;
}

One issue with the solution above is that the normals get skewed when the world matrix gets scaled. This can be fixed by using the inverse transpose of the world matrix instead.

#version 300 es

in vec4 a_position;
in vec3 a_normal;

uniform mat4 u_viewProjMat;
uniform mat4 u_worldMat;
uniform mat4 u_invTransWorldMat;

out vec3 v_normal;

void main() {
	mat4 mat = u_viewProjMat * u_worldMat;

	gl_Position = mat * a_position;

	v_normal = mat3(u_invTransWorldMat) * a_normal;
}

The example below doesn't look any different because the cube isn't being scaled, but it uses the world inverse transpose matrix instead of just the world matrix.

Colored lighting

The light can be given a color by adding a color to the calculation.

#version 300 es

precision highp float;

in vec3 v_normal;

uniform vec4 u_color;
uniform vec3 u_reverseLightDirection;
uniform vec4 u_lightColor;

out vec4 outColor;

void main() {
	vec3 normal = normalize(v_normal);

	vec3 light = u_lightColor.rgb
		* dot(normal, u_reverseLightDirection);

	outColor = u_color;
	outColor.rgb *= light;
}

Ambient lighting

Ambient lighting is lighting that is applied equally to all triangles, regardless of their normal vector. This is used to set a "minimum" brightness to ensure that nothing in the scene is too dark.

#version 300 es

precision highp float;

in vec3 v_normal;

uniform vec4 u_color;
uniform vec3 u_reverseLightDirection;
uniform float u_ambientLight;

out vec4 outColor;

void main() {
	vec3 normal = normalize(v_normal);

	float directionalLight =
		dot(normal, u_reverseLightDirection);

	outColor = u_color;
	outColor.rgb *= directionalLight + u_ambientLight;
}

Point lighting

Point lighting is when light extends in every direction from a point. To emulate it, pass the position of the light to the vertex shader as a uniform, then pass the distance from the light source to the surface as a varying.

#version 300 es

in vec4 a_position;
in vec3 a_normal;

uniform mat4 u_viewProjMat;
uniform mat4 u_worldMat;
uniform mat4 u_invTransWorldMat;
uniform vec3 u_lightPos;

out vec3 v_normal;
out vec3 v_lightDir;

void main() {
	mat4 mat = u_viewProjMat * u_worldMat;

	gl_Position = mat * a_position;

	v_normal = mat3(u_invTransWorldMat) * a_normal;

	// Determine the position of the surface.
	vec3 surfacePos = (u_worldMat * a_position).xyz;

	// Pass the direction from the surface to the light.
	v_lightDir = u_lightPos - surfacePos;
}
#version 300 es

precision highp float;

in vec3 v_normal;
in vec3 v_lightDir;

uniform vec4 u_color;

out vec4 outColor;

void main() {
	vec3 normal = normalize(v_normal);
	vec3 lightDir = normalize(v_lightDir);

	float light = dot(normal, lightDir);

	outColor = u_color;
	outColor.rgb *= light;
}

Notice that the code for point lighting is almost identical to the point for directional lighting. The only difference is that the light direction is interpolated between fragments.

Specular lighting

Specular lighting is when light reflects off of a shiny object. Light reflects off of a surface at the same angle that it hits that surface, so it is possible to determine whether specular light should be rendered if the angle from the light to the surface is the same as the angle from the surface to the camera.

As explained above, the angle between two vectors can be determined using the dot product. Then, if the two dot products are added together and normalized, you get the half vector (the vector that sits halfway between them). The brightness of the specular light can then be determined based on how similar the half vector is to the surface's normal (again using the dot product).

#version 300 es

in vec4 a_position;
in vec3 a_normal;

uniform mat4 u_viewProjMat;
uniform mat4 u_worldMat;
uniform mat4 u_invTransWorldMat;
uniform vec3 u_lightPos;
uniform vec3 u_camPos;

out vec3 v_normal;
out vec3 v_lightDir;
out vec3 v_camDir;

void main() {
	mat4 mat = u_viewProjMat * u_worldMat;

	gl_Position = mat * a_position;

	v_normal = mat3(u_invTransWorldMat) * a_normal;

	vec3 surfacePos = (u_worldMat * a_position).xyz;

	v_lightDir = u_lightPos - surfacePos;

	v_camDir = u_camPos - surfacePos;
}
#version 300 es

precision highp float;

in vec3 v_normal;
in vec3 v_lightDir;
in vec3 v_camDir;

uniform vec4 u_color;

out vec4 outColor;

void main() {
	vec3 normal = normalize(v_normal);
	vec3 lightDir = normalize(v_lightDir);
	vec3 camDir = normalize(v_camDir);
	vec3 halfVector = normalize(lightDir + camDir);

	float pointLight = dot(normal, lightDir);

	float specularLight = dot(normal, halfVector);

	outColor = u_color;
	outColor.rgb *= pointLight;
	outColor.rgb += specularLight;
}

The shine can be made to look more realistic by raising the angle between the normal and the half vector to a certain power that represents the "shininess" of the surface. This has the effect of changing the specular light from a linear falloff to an exponential falloff.

#version 300 es

precision highp float;

in vec3 v_normal;
in vec3 v_lightDir;
in vec3 v_camDir;

uniform vec4 u_color;
uniform float u_shininess;

out vec4 outColor;

void main() {
	vec3 normal = normalize(v_normal);
	vec3 lightDir = normalize(v_lightDir);
	vec3 camDir = normalize(v_camDir);
	vec3 halfVector = normalize(lightDir + camDir);

	float pointLight = dot(normal, lightDir);

	// `pow` is undefined on negative numbers in WebGL.
	float specularLight = pointLight >= 0.0
		? pow(dot(normal, halfVector), u_shininess)
		: 0.0;

	outColor = u_color;
	outColor.rgb *= pointLight;
	outColor.rgb += specularLight;
}

Spot lighting

Spot lighting is almost the same thing as point lighting, with the difference being that the light only extends from the point in a cone. To emulate spot lighting, WebGL needs to know the direction of the cone and the angle from that direction to the sides of the cone (referred to as the "limit"). From the limit, we can create a dot limit by taking the cosine of the limit. If the dot product of the direction to the surface is above the dot limit, the surface is within the cone.

#version 300 es

precision highp float;

in vec3 v_normal;

/*
`v_lightDir` represents the direction from the current
fragment to the light.
*/
in vec3 v_lightDir;

uniform vec4 u_color;

/*
`u_reverseLightDir` represents the opposite of the
direction that the light is facing, just like with
directional lighting.
*/
uniform vec3 u_reverseLightDir;

uniform float u_limit;

out vec4 outColor;

void main() {
	vec3 normal = normalize(v_normal);
	vec3 lightDir = normalize(v_lightDir);

	float light =
		dot(lightDir, u_reverseLightDir) >= u_limit
			? dot(normal, lightDir)
			: 0.0;

	outColor = u_color;
	outColor.rgb *= light;
}

It is also possible to set an outer limit and an inner limit and interpolate the brightness between them to give the spotlight a smooth edge.

#version 300 es

precision highp float;

in vec3 v_normal;
in vec3 v_lightDir;

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 lightDir = normalize(v_lightDir);

	float angularDist = dot(lightDir, u_reverseLightDir);

	// `smoothstep` uses a Hermite interpolation.
	float light =
		smoothstep(u_outerLimit, u_innerLimit, angularDist)
		* dot(normal, lightDir);

	/*
	// This could also be done with a linear interpolation:
	float limitRange = u_innerLimit - u_outerLimit;
	float light =
		clamp((angularDist - u_outerLimit) / limitRange,
			0.0, 1.0)
		* dot(normal, lightDir);
	*/

	outColor = u_color;
	outColor.rgb *= light;
}

The next article is about cubemaps.