Warning: Some maths is ahead, I'm sorry, but it is impossible to avoid it in order to describe the ideas clearly.
Working It Out
I am currently experimenting with a way to add a sense of 'shininess' to models. I was ready to release 3DMatrix but now that all the hard work is done (including most of the documentation) I wanted to add a few little bits.. Isn't this the way all products should be released? You plan to do a, b, c and d, but when you reach d, you very much want to do e also.
I am assuming that all questions about polygon projection are covered and our goal here is working out how brightly to color each polygon in any scene.
With the 3DMatrix product I only considered how precisely each polygon was facing towards each light source, and the more exactly a polygon was facing towards the light the brighter it should be colored. The brightness is also adjusted by the ceofficient Log(1 + 1/D^2) where D is the distance between the polygon's center and the light source, however I will be ignoring this coefficient with all of the brightness calculations in order to keep the explanations cleaner. Just keep in mind that each brightness expression should also be multiplied by Log(1 + 1/D^2). Note also that n, l and r will always be used denote 3D vectors.
The diagram above shows an arbitrary polygon with its direction determined by the normal unit vector n. If the polygon is facing exactly towards the light then L = 0 and this is when we want the maximum brightness. If the L = 90 degrees then no light is hitting the polygon so we want the minimum brightness. It is a well known trick to use exactly CosL as the polygon's brightness. Cos0 is 1 (maximum brightness) while Cos90 = 0 (minimum brightness), and CosL can be calculated very quickly as the dot product of unit vectors l and n. It is necessary to mention that if CosL < 0 then we want to use 0 in replace of CosL, because once the polygon is rotated away from the light, we don't want it to start having a negative brightness and instead it simply receives no light. For this reason, whenever I use the function Cos with a capital C below I am referring to the special cosine function that is replaced with zero when negative.
In trying to acheive a sense of shininess, I initially had the idea to raise this brightness calculation to the power of a high number so that intensity really only increases when L is very close to zero. We could replace CosL with CosL ^ 30. This would make the brightest parts of the scene more bright and the darker parts more dark and I was interested to see whether this would make the surfaces appear more shiny.
Let's compare two images of the same supertoroid, firstly using CosL for the brightness and secondly replacing CosL with CosL^30.
Considering that replacing CosL with CosL ^ 30 doesn't really slow down the light calculation I thought this was a good start. It really looks like the surface of the object is more shiny, and we have such a result with many frames per second which can still be used in real-time applications. All we are doing is making the brightest part brighter and the less bright parts even less bright.
However, I quickly realized that this is not exactly what the true meaning of shininess is!!!
Look around. With shininess you don't care about how exactly the polygon is facing towards the light, but you are interested in the reflected light. If you look at the table in front of you, ask yourself when is it the most bright? It is brightest not when the light is directly above it (as the CosL calculation above uses) but when it is sort of away from you such that the light is reflected straight back into your eyes. So I went to work in trying to improve the above calculation by adding a reflection component and for this we have to take into account the position of the camera.
I'm sorry, this made the diagram look less friendly. But it is really not complicated. I'm just adding an extra vector for the camera so that we can consider how closely the light reflected off the polygon is in the direction of the camera. This angle of interest is R in the diagram above, so we can calculate the reflection brightness as CosR, which as the maximum value is when R = 0.
It would be nice to be able control the proportion of the the two types of light calculations, CosL and CosR. This can be done by improving the brightness formulae as follows:
brightness = intensityL * CosL + intensityR * CosR
For example, a mirror is purely reflected light, so intensityL would be zero. By contrast, something like a brick has almost purely dispersed light and so intensityR would be set to zero to represent such a surface. The brightest side of the brick is the side pointing the most closely to the sun and as you move around the brick each side does not change in brightness, so there is little or no reflected light. Animators would be interested to observe that the value of CosR changes as the camera moves (ie. as you move around the brick) while the value of CosL has nothing to do with the position of the camera.brightness = intensityL * CosL + intensityR * (CosR ^ shininess)
This formulae looks fairly good. I decided to raise only the reflected brightness to the power of shininess rather than raising the direct brightness to any power as I did in Image 2 above. It doesn't make much sense to make the CosL lightness calculation highly sensitive to direction. If you tilt a brick away from the sun slightly, the surface brightness (CosL) hardly changes, but for a smooth object that reflects light, tilting the object changes the reflection highlights enormously. Also, the smoother the object, the more sensitive to direction the reflected light should be, and for this reason it would be quite reasonable to replace the word 'shininess' in the formulae above with the word 'smoothness'.
So what do we have to do to calculate the formulae above? CosL has been explained so we just need to calculate CosR. CosR is the dot product of r and c, and we know c straight away as the vector between the polygon's center and the camera so the hard part is finding r. This caused me to pause for a while but I managed to work it out without resorting to asking my maths Professor friend Terry Tao which I try to save only for the absolutely uber-crack problems.
The trick in calculating the vector r is to determine PN above, which is the projection of the vector l onto the unit vector n. This is the fast calculation (l.n)n, where both n and l are unit vectors. But l.n is CosL, already calculated, so PN is simply CosL * n. Now that we know PN, then it is trivial to calculate r because AN = NB from what it means to have a reflection and geometrically we can see that r = l + 2*AN, thus r = l + 2(-l + PN) and so r = 2*PN - l, and thus we have:
r = 2 * CosL * n - l
This turned out very nicely indeed because we can calculate the reflection vector increadibly quickly (we already know CosL, n and l). The computer only needs to do two multiplications and a subtraction for each of the the x, y and z components of r. Since r is already a unit vector we can calculate CosR as r.c with an extra three multiplications.
Results
It is appealing how quickly you can move from theory to practice with pure mathematics. In the office I was looking at the light around the room to come up with the formulaes above and then when I arrived home implemented this improved version of shininess according to the theory. A couple of syntax errors, and then bang, the image appeared in front of me.
There we have it! I think the result of added real reflected light is spectacular so I'm absolutely thrilled to have gone through with all that maths.
Let's make some observations about how this image compares to the previous two: 1. The brightest regions for the reflected light are different to the brightest regions for the direct light. This is what happens in nature, and is why the last image makes the previous two suddenly look so artificial. 2. The front face of the supertoroid is completely flat and in the first two images it was almost all the same brightness while with the third image we can see different shades of brightness over the same flat surface. 3. Another observation is impossible to show with a single image, but as you move the camera forwards and backwards, the position of the reflected CosR highlights crawl along the supertoroid while the highlights associated with the CosL calculations stay in the same place.
You can also iterate the same formulae with multiple lights. For each polyon you can include an inner loop that takes each light in the scene and calculates CosL and CosR and thus brightness, and overallBrightness is simply the sum of the brightness calculations for each light. This means that having two lights instead of one will double the number of light calculations. The following example uses three different light sources with slightly different colours.
We also have a lot more variables to play with in the modified brightness formulae. I could increase the shininess value for example:
Now if that does not look shiny, then I don't know what does. Compared with the very first image which was the best I could to represent a metal surface with 3DMatrix the morning earlier, I think we can say objective acheived.
I'll shortly formalize these parameter options within a user-friendly interface so that 3DMatrix users can play around with the settings at a polygon-group level.
The following experiment demonstrates the nature of the reflective highlights finding their way into view more easily than the diffuse highlights (brightest part of the CosL light component). For example, the three lights in this scene are almost as far apart as they can be, however in each sphere you can still see the highlights from all three lights on one side of the sphere! This is a beautiful property of reflected light - the reflected highlights invite themselves to enter the scene more easily than the very brightest region of the CosL calculations.
Well, that's about it for Shininess. I encourage you to have some fun by downloading 3DMatrix and experimenting with entering different values within the formulaes above, and getting a feel for how these calculations can be executed fast enough for real-time motion.
Julian Cochran