After I had implemented moving the sun a few weeks ago, I realized that things looked out of place without an actual sun. So I busted out the physics textbook (google) and did some research.
The Physics
You might remember from your physics classes that light does one of three things when it strikes an object.
It can be absorbed, reflected, or transmitted into the new medium. In the case of atmospheric scattering,
we discount the last possibility because all of the particles are opaque. This leaves us with absorption and
reflection. Absorption is fairly self explanatory, the farther light has to travel through the atmosphere, the
less light is going to make it. Reflection is not quite as simple. Because the light could be reflected in any
direction off of the particle, we further define
reflection as in-scattering and out-scattering. Out-scattering is when light originally in a ray is reflected (scattered)
away from (out of) the ray, lessening the final brightness. In-scattering is when light not originally out of a ray
is scattered into the ray, increasing the final brightness. To define the final color of a fragment
to a viewer, these three values most be calculated for every point along a ray from the fragment to the viewer. To see
this in action, lets look at a sky without scattering.
If we consider the sun as a directional light source where all rays are parallel (not entirely true, but close enough for most purposes)
then for each ray, we need only check if it is perfectly parallel to the sun, if it is then we see a bright sun at that ray, if not then
there is no light and we see black. This is what you see if you look at the sun from space (Because the real world sun is not a perfect
directional light source, there are several rays which are parallel to the sun, giving it its apparent size).
Absorption
The first atmospheric affect we look at is absorption. During each collision with a particle, there is a probability that
the light is absorbed. The further that light has to travel through the atmosphere, the higher the probability of a collision, and the more
collisions there are, the more chances there are for that light to be absorbed. If it were possible to observe a planet where the atmosphere
only absorbed light, you would still see the sun at a single point, but as it lowered in the sky the point would become dimmer as more light is
absorbed by the atmosphere.
Out-Scattering
Next we introduce out-scattering, the effect will be similar to that of absorption, as light is removed from the ray but the process is slightly
different. To examine it we need to look at the two types of scattering, Mie scattering and Rayleigh scattering. The two are different names
for similar phenomena. Mie scattering is seen when the size of the particles doing the scattering is comparable to the wavelength of the light being scattered,
such as water vapor in clouds, or smog and other large particles close to the surface. Rayleigh scattering is when the particles doing the scattering are
much smaller than the wavelength of the light, such as the nitrogen in the atmosphere. Going to much further dives into some serious physics, but the
important thing to take away is that Rayleigh scattering scatters shorter wavelengths (blue) more than it does longer wavelengths (red), while Mie scattering
scatters all wavelength equally. During sunrise and sunset, the light from the sun has more atmosphere to travel through, so much of the blue light is scattered away, giving the dawn and dusk sun its red color. During the day, there is less atmosphere between your eyes and the sun to scatter away the blue light, and we are able to see it as the color of the sky.
Implementation
In reality, every ray of light is eventually absorbed by something. Unfortunately, simulating the lifespan of every individual ray of light, potentially through dozens of bounces through the atmosphere can be incredibly computationally intensive. To make calculations easier, instead of simulating every bounce, we assume that once a ray has been out scattered, it effectively disappears. Now we can combine these two effects into a single factor, extinction, and model it as an exponential decay.
vec3 extinction(float dist, vec3 initial_light, float factor) {
return initial_light - initial_light * pow(scatter_color, vec3(factor / dist));
}
scatter_color
is what really defines the color of the sky, you can modify it in your config file by setting scatterColor = #xxxxxx
. This color specifies how the sky scatters different colors of light. The gist of it is that a higher a channel is set, the more of it you will see in day time, and the less you see during sunrise and sunset.
Now we’re getting somewhere, but we need some more support equipment if we want to render a sky. To calculate the amount of light that is lost to extinction, we need to know how much atmosphere the light is traveling through. This function is the product of the law of sines from trigonometry and a circular cross section of the atmosphere through the center of the earth, the viewer, and the point where the a ray from the viewer leaves the atmosphere.
float atmospheric_depth(float alt, vec3 dir) {
float d = dir.y;
return sqrt(alt * alt * (d * d - 1) + 1) - alt * d;
}
We have everything we need to define out-scattering and absorption, but we still need a way to define in-scattering. To calculate in-scattering, we use a phase function. This function estimates the probability that a ray will be scattered an angle alpha
away from its original direction. This phase function comes from this GPU Gems article.
float phase(float alpha, float g) {
float a = 3.0 * (1.0 - g * g);
float b = 2.0 * (2.0 + g * g);
float c = 1.0 + alpha * alpha;
float d = pow(1.0 + g * g - 2.0 * g * alpha, 1.5);
return (a / b) * (c / d);
}
To color the rest of the sky, we sample several points along a vector from the eye to the fragment we are trying to color and calculate the probability that light will be scattered from the sun towards the viewer. We scale the probability by the intensity of the sun to calculate the amount of light being scattered from the sun towards the viewer. But this light isn’t done yet, it still has some atmosphere to travel through, so we calculate the amount of light that is lost going from the sample to the viewer. We add together the light from each sample point to find the total light that reaches the viewer. You can read the rest of the code here, and have a look at Florian Boesch’s excellent article where I pulled the original algorithm from.