In the previous article we covered directional lighting where the light is coming
universally from the same direction. We set that direction before rendering.
What if instead of setting the direction for the light we picked a point in 3d space for the light
and computed the direction from that point to each visible spot on the surface of our model in our shader?
That would give us a point light.
If you rotate the surface above you’ll see how each point on the surface has a different
surface to light vector. Getting the dot product of the surface normal and each individual
surface to light vector gives us a different value at each point on the surface.
So, let’s do that.
First we need the light position
structUniforms{
normalMatrix: mat3x3f,
worldViewProjection: mat4x4f,
color: vec4f,
lightDirection: vec3f,
lightPosition: vec3f,
};
And we need a way to compute the world position of the surface. For that we can multiply
our positions by the world matrix so …
structUniforms{
normalMatrix: mat3x3f,
worldViewProjection: mat4x4f,
world: mat4x4f,
color: vec4f,
lightDirection: vec3f,
lightPosition: vec3f,
};
....
// Compute the world position of the surface
let surfaceWorldPosition =(u_world * vert.position).xyz;
And we can compute a vector from the surface to the light which is similar to the
light direction we had before except this time we’re computing it for every position on the
surface to a light’s world position point.
Now in the fragment shader we need to normalize the surface to light vector
since it’s a not a unit vector. Note that we could normalize in the vertex shader
but because it’s an inter-stage variable it will be linearly interpolated between our positions
and so would not be a complete unit vector
Now that we have a point we can add something called specular highlighting.
If you look at on object in the real world, if it’s remotely shiny, then if it happens
to reflect the light directly at you it’s almost like a mirror
We can simulate that effect by computing if the light reflects into our eyes. Again the dot-product
comes to the rescue.
What do we need to check? Well let’s think about it. Light reflects at the same angle it hits a surface
so if the direction of the surface to the light is the exact reflection of the surface to the eye
then it’s at the perfect angle to reflect
If we know the direction from the surface of our model to the light (which we do since we just did that).
And if we know the direction from the surface to view/eye/camera, which we can compute, then we can add
those 2 vectors and normalize them to get the halfVector which is the vector that sits half way between them.
If the halfVector and the surface normal match then it’s the perfect angle to reflect the light into
the view/eye/camera. And how can we tell when they match? Take the dot product just like we did
before. 1 = they match, same direction, 0 = they’re perpendicular, -1 = they’re opposite.
So first thing is we need to pass in the view/camera/eye position, compute the surface to view vector
and pass it to the fragment shader.
Next in the fragment shader we need to compute the halfVector between
the surface to view and surface to light vectors. Then we can take the dot
product the halfVector and the normal to find out if the light is reflecting
into the view.
We can fix the brightness by raising the dot-product result to a power. This will scrunch up
the specular highlight from a linear falloff to an exponential falloff.
The closer the red line is to the top of the graph the brighter our specular addition
will be. By raising the power it scrunches the range where it goes bright to the
right.
Let’s call that shininess and add it to our shader.
pow(specular, uni.shininess),// value if condition is true
specular >0.0);// condition
The dot product can go negative. Taking a negative number to a power is undefined in WebGPU (or is NaN?) which would be bad. So, if the dot product is negative then we just leave specular at 0.0.