Eye Space Lighting
When this renderer first loads, the shading indicates that the light source is positioned above the torus:
However, when we rotate just the torus so that it flips over, the shading suggests that the light source is below the torus, even though the light source's yellow orb hasn't moved.
The problem is these lines in the fragment shader:
const vec3 lightPosition = vec3(0.0, 10.0, 0.0);
in vec3 mixPosition;
in vec3 mixNormal;
const vec3 lightPosition = vec3(0.0, 10.0, 0.0); in vec3 mixPosition; in vec3 mixNormal;
What space are these in? Clip space? Eye space? World space? Model space? Are they even in the same space? When we don't consider the spaces of our coordinates, we end up with strange behavior.
The vertex shader you assembled on the previous page implicitly determines the space of mixPosition
and mixNormal
. Note that there are no transformations in their assignments. The interpolated values are therefore in the same space as position
and normal
.
The fragment shader has this line:
vec3 lightDirection = normalize(lightPosition - mixPosition);
vec3 lightDirection = normalize(lightPosition - mixPosition);
The subtraction implies that lightPosition
is also in model space. Since lighting is being performed in the untransformed model space, rotating the torus has no effect on the shading. The model space shading sticks to the torus as it is transformed into its world space orientation.
Rarely do we want to perform lighting in model space. Light sources are usually position in either world space or eye space. Lamp posts, wall-mounted torches, and other fixtures are typically positioned in world space because they are anchored to some position in the world. Flashlights in the viewer's hands are typically positioned in eye space because they move around with the viewer.
Whether our lights are positioned in world space or eye space, the actual shading calculations tend to be performed in eye space. This is because some of the lighting terms we're about to discuss involve the position of the eye. In eye space, the position of the eye is predictably at \(\begin{bmatrix}0&0&0\end{bmatrix}\).
Therefore, the fragment position and normal must both be transformed to eye space. We transform them in the vertex shader:
The suffix Eye
has been appended to the identifiers to explicitly indicate their space. Note that the homogeneous coordinate for the normal is 0 instead of 1. Recall that we added a homogeneous coordinate to make translation work as a matrix multiplication. Vectors are mere directions, and we do not want them to translate. The 0 cancels out the translation offsets.
The light position must also be in eye space. This fragment shader is almost identical to the one that performed lighting in model space, except lightPosition
has been renamed lightPositionEye
and turned into a uniform:
uniform vec3 lightPositionEye;
in vec3 mixPositionEye;
in vec3 mixNormalEye;
out vec4 fragmentColor;
void main() {
vec3 lightDirection = normalize(lightPositionEye - mixPositionEye);
vec3 normal = normalize(mixNormalEye);
float litness = max(0.0, dot(normal, lightDirection));
fragmentColor = vec4(vec3(litness), 1.0);
}
uniform vec3 lightPositionEye; in vec3 mixPositionEye; in vec3 mixNormalEye; out vec4 fragmentColor; void main() { vec3 lightDirection = normalize(lightPositionEye - mixPositionEye); vec3 normal = normalize(mixNormalEye); float litness = max(0.0, dot(normal, lightDirection)); fragmentColor = vec4(vec3(litness), 1.0); }
In this renderer, the light source is fixed at \(\begin{bmatrix}2&2&8\end{bmatrix}\) in world space:
Its world space position is converted to eye space just once by the CPU and uploaded as a uniform with this TypeScript code:
const vec3 lightPositionWorld = new Vector3(2, 2, 8);
const vec3 lightPositionEye = eyeFromWorld
.multiplyMatrix(worldFromModel)
.multiplyVector(lightPositionWorld.toVector4(1));
shader.setUniform3f('lightPositionEye', lightPositionEye.x, lightPositionEye.y, lightPositionEye.z);
const vec3 lightPositionWorld = new Vector3(2, 2, 8); const vec3 lightPositionEye = eyeFromWorld .multiplyMatrix(worldFromModel) .multiplyVector(lightPositionWorld.toVector4(1)); shader.setUniform3f('lightPositionEye', lightPositionEye.x, lightPositionEye.y, lightPositionEye.z);
As we rotate the torus, only the faces pointing toward the light source are illuminated by the light source. This is the behavior we want.