WebGPU game (#12): Diffuse lighting
Previously, shadow maps were added to the game. However, the game still had a feeling of being a bit too flat. This is because shadow mapping only accounts for the occlusion of one object by another. It does not simulate the effect of objects being darker at grazing angles to a source of light. If you have a window near you, open the palm of your hand towards it. It should be maximally bright. Once you start rotating your palm away from the light – even without occluding your hand by itself – you’ll notice your hand darkens.
Diffuse lighting
This is actually much easier to model than shadow maps. For diffuse lighting, all we need is a normal () and the direction of the light source (). With these, we want some function where:
This is essentially the dot product, except the sign of the light direction needs to be flipped. It’s easier to work with the direction to the light rather than from the light. For angles greater than , the value is less than zero, so we simply use the built in WGSL clamp function to ensure our ratio is between zero and one1.
Transforming the normal
Next, we will need the normal in world space to compare it to the light direction, which is defined in world space. Moving a vertex into world position requires multiplying it by the model matrix. However, the normal should not be affected by translation, so we should zero out the first 3 elements of the last column in the model matrix. This resultant matrix would be sufficient for use in this game, but consider the case below:
A valid model transform may scale some object along one axis, such as the -axis in the example above. However, this results in the transformed normal, , being incorrect. We need some new normal, . So, we need some other matrix to correct the potential change of the original normal. First, we need to introduce some symbols:
Symbol | Description |
---|---|
Model matrix | |
Unknown matrix to correctly transform our normal, | |
Tangent to the normal in local space (represents a vector along any arbitrary plane of the model) | |
Tangent after the transformation by the model matrix (in other words, in world space) |
So, from the definitions we have:
We want to determine the transformation matrix, , that ensures the transformed normal and transformed tangent ( and ) are perpendicular:
By definition, . So, we know that must be the identity matrix (). This means we can determine the transform we need, , as:
I read this as the “inverse transpose model matrix”. The LearnOpenGL article excludes this derivation, but derivation/proof tends to help me retain information for longer. There are a few resources that explain this in a few different ways, so I’ll list them here:
- https://www.lighthouse3d.com/tutorials/glsl-12-tutorial/the-normal-matrix/
- https://en.wikipedia.org/wiki/Normal_(geometry)#Transforming_normals
The one thing these resources both mention is that the inverse transpose is equal to the original matrix if the original is an orthonormal matrix (because ).
This follows from the intuition at the beginning. If only uniform scaling and rotation is applied, then it is the model matrix (without the translation). Note, however, that scaling removes the orthonormality of the matrix – so, we have to normalize the resultant vector to ensure the normal is a normal.
Putting it all together
With all of this, we have enough theory established to implement the diffuse lighting in the game. In summary, we need to make the following changes:
- Add the light direction to the uniforms.
- Add the model inverse transpose matrix () to the instance buffer.
- Add the normal to the
Vertex
class and the vertex buffer (Make changes as necessary to the constructor and methods that create vertices). - Use all of the above to calculate the diffuse factor and apply it to the shadow factor2.
If you’ve been following along, consider trying the above for yourself. Previous posts have covered similar changes. Otherwise, for the full change, see the link to the Git tree below. For a comparison, here is the before-and-after for the game with and without shadows and diffuse lighting:
Before | After |
---|---|
Links
Footnotes
-
There are many resources for this on the internet, for example: https://learnopengl.com/Lighting/Basic-Lighting. ↩
-
I used multiplication. It may actually make more sense to use the minimum of the two factors. ↩