Note: I am going to talk about lighting as a color range between (0.0-1.0) here, not (0-255).
Lighting stacks additively. If you have two identical lights at the same distance from a given point, the point becomes twice as bright as if only one light had been there. In addition, the light reaching a given pixel should be multiplied by the unlit color of the pixel. You could write it as:
Vector3 pixelColor = ...;
Vector3 litPixel = (0, 0, 0);
for(Light light : lightList){
lightStrength += pixelColor * light.color * attenuation;
}
This has one glaring problem. When the light intensity of a color channel reaches 1.0 (or 255), you get a very ugly area where the color is obviously clamped. In the following image you can clearly see the area where the light intensity was clamped to 1.
However, although the physical light intensity may double from having two lights, that does not mean that our eyes actually see it as twice as bright. Our eyes are much more sensitive at lower light intensities and we can’t very well differentiate between two different bright intensities. With our monitors only being able to show 256 different levels of brightness for each pixel, visualizing extremely bright lights is difficult. To be able to show extremely bright lights, you really need to use HDR, High-Dynamic Range, which is just fancy word using floating point pixel colors that can exceed 1.0 instead of 8-bit colors restricted to 1.0. After accumulating all light, you run the pixel colors through a tone-mapping function to reduce them to the (0.0-1.0). The most simple operator is (color / (color+1)), which converts values like this:
0.0 --> 0.0
0.5 --> 0.3333
1.0 --> 0.5
1.5 --> 0.6
2.0 --> 0.6666
3.0 --> 0.75
5.0 --> 0.8333
As you can see, as the raw color approach infinity the displayed color approaches 1.0, the maximum we can display. That ensures that there is always a “brighter” value to display if the light is brighter and gives a much smoother curve compared to just clamping the values at 1.0.
Another big problem: I’m assuming that your ground is very close to pure green and that your light is pure red. In this case, when you multiply together these two colors you get (0, 1, 0)*(1, 0, 0), which is equal to 0. It is VERY important to always have a little bit of each color so that extremely bright points can converge to white. Look at this picture:
http://blogg.svt.se/melodifestivalen/files/2014/02/scen.jpg
The light shafts and especially the lights all converge to white if you check the actual colors in the image. Our brains still understands what color this bright “white” is supposed to have based on the colored bloom/halo around the pixel. With the above tone-mapping operator, this effect is actually achieved:
(0.5, 0.1, 0.1) —> (0.333, 0.09, 0.09), not heavily modified and clearly red.
(50, 10, 10) —> (0.98, 0.91, 0.91), very close to white with a slight red tint.
This however assumes that neither the ground color nor the light color is a pure color. If any of these two colors’ color channels are 0, the result for that channel WILL be zero preventing the fade to white.