Lighting

You may need to read the articles about 3D and varyings before this one.


Directional 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. For example:

#version 300 es

in vec4 a_position;
in vec3 a_normal;

uniform mat4 u_viewProjection;
uniform mat4 u_world;
uniform mat3 u_normal;

out vec3 v_normal;

void main() {
	gl_Position = u_viewProjection * u_world * a_position;
	v_normal = u_normal * a_normal;
}

Notice that the normals are multiplied by a matrix called u_normal. A normal matrix is the inverse transpose of the rotation portion of a transformation matrix. The upper-left three-by-three part 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 to the light source is the opposite of the direction that the light is traveling. For example:

#version 300 es

precision highp float;

in vec3 v_normal;

uniform vec3 u_reverseLightDirection;
uniform vec4 u_color;

out vec4 outColor;

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

	float brightness = dot(normal, u_reverseLightDirection);

	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.

Colored Lighting

A light source can be given a color by multiplying the brightness by that color. For example:

#version 300 es

precision highp float;

in vec3 v_normal;

uniform vec3 u_reverseLightDirection;
uniform vec4 u_color;
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 surfaces regardless of normal. It is used to set a minimum brighness so that nothing in the scene is too dark. For example:

#version 300 es

precision highp float;

in vec3 v_normal;

uniform vec3 u_reverseLightDirection;
uniform vec4 u_color;
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 direction from the surface to the light source as a varying. For example:

#version 300 es

in vec4 a_position;
in vec3 a_normal;

uniform mat4 u_viewProjection;
uniform mat4 u_world;
uniform mat3 u_normal;
uniform vec3 u_lightPosition;

out vec3 v_normal;
out vec3 v_directionToLight.

void main() {
	gl_Position = u_viewProjection * u_world * a_position;

	v_normal = u_normal * a_normal;

	vec3 surfacePosition = (u_world * a_position).xyz;
	v_directionToLight = u_lightPosition - surfacePosition;
}

Point lighting is almost identical to directional lighting, except that the light direction is interpolated between fragments.

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 the same as 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 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" value to change the specular lighting from a linear to an exponential falloff, which makes it look more realistic. For example:

#version 300 es

precision highp float;

in vec3 v_normal;
in vec3 v_directionToLight;
in vec3 v_directionToCamera;

uniform vec4 u_color;
uniform float u_dullness;

out vec4 outColor;

void main() {
	vec3 normal = normalize(v_normal);
	vec3 directionToLight = normalize(v_directionToLight);
	vec3 directionToCamera = normalize(v_directionToCamera);
	vec3 halfVector = normalize(directionToLight + directionToCamera);

	float pointBrightness = dot(normal, directionToLight);
	float specularBrightness = pointBrightness >= 0.0
		? pow(dot(normal, halfVector), u_dullness)
		: 0.0;

	outColor = u_color;
	outColor.rgb *= pointBrightness;
	outColor.rgb += specularBrightness;
}

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. For example:

#version 300 es

precision highp float;

in vec3 v_normal;
in vec3 v_directionToLight;

uniform vec4 u_color;
uniform vec3 u_reverseLightDirection;
uniform float u_innerLimit;
uniform float u_outerLimit;

out vec4 outColor;

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

	float angularDistance =
		dot(directionToLight, u_reverseLightDirection);
	float brightness =
		smoothstep(u_outerLimit, u_innerLimit, angularDistance)
		* dot(normal, directionToLight);

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

The next article is about cubemaps.