Create a fog shader

I decided to improve my last OpenGL scene by adding fog to create an odd mystique effect. Now I’m sure that all of you have seen fog in your lives and I don’t have to explain what it is. If you want to read more about the fog phenomenon and its characteristics you can read more on Wikipedia or other sources. In Computer Graphics you can create a fog effect in a few ways. In this tutorial I will focus on the standard way to create fog in shaders. At the end of the article we can improve our fog effect just by adding scattering and create a cool ground fog. I saw lots of tutorials about fog on the Internet and lots of them are deprecated so I hope I can bring a bit of fresh air.

Scene with fog

My scene with fog

Because this article is long I will structure it like this:

  • Clear color
  • Standard fog equations and differences between them
  • Getting the distance: Z-Depth vs Length
  • Fog in vertex shader vs Fog in fragment shader
  • Beautiful fog with atmospheric effects
  • Further reading

Clear color

You have to choose wisely the clear color of your OpenGL/DirectX scene because this can have a major impact over visual aspects. Usually for a static scene you can choose the clear color to have the same RGB like your fog color. Yes, fog color can have any RGB and of course it’s not required to be always gray(0.5,0.5,0.5). For example in World of Warcraft uses colored fog, based on the zone you are in (Light Blue Fog, Purple FogOrange Fog).

clear color comparison

a) Clear color same as fog color
b) Blackish clear color

//glClearColor(0.05,0.03,0.00,1);//blackish color
glClearColor(0.5, 0.5, 0.5, 1);//gray color, same as fog color
glClearDepth(1);
glEnable(GL_DEPTH_TEST);

Standard fog equations and differences between them

There are three standard fog equations that we can use in our scene to create a fog effect. They are called “standard” because they were here to help us with fog back when the Fixed Pipeline was used.

Linear fog is the simplest fog equation; it’s a linear interpolation function which blends the fog color with original color based on the distance you view the objects from your scene. We need two parameters to create this function. The first one is the fogStart parameter or minimum distance where we need to provide information where the fog should start to blend, and of course the scond one is the fogEnd or the maximum distance where the fog color will block (cover) the original color from the viewer. So the function should look like this:

linear fog equation

linear fog equation

We can call ƒ the fogFactor and this value should give a value between 0.0 and 1.0 to get a correct mix between original color and fog color, so we need to clamp it between 0.0 and 1.0.

linear fog result

Linear fog result

I created another camera with the view from above the scene just to see how the fog behaves: 

linear fog explained

Linear fog;  fog factor function

Exponential fog is a simplified model from real world phenomenon so it has a physical background behind. Light intensity is reduced over the distance due to absorbtion and scattering from air particles. So, for each particle the light ray will reach, its intensity will be absorbed by a small attenuation factor. We can think of these particles as very small steps. For each step the light intensity is absorbed by this attenuation factor. This leads to an exponential behavior, which tell us that light intensity decreases exponentially with the distance from the object. Why is this model simplified? Because of that attenuation factor we choose to be constant. In the real world this factor can vary from particle to particle; check the Beer-Lambert law in atmosphere to see how many terms can influence this attenuation factor. For our model this attenuation factor is called fog density and as I said earlier it is uniform. The result can be seen in the first image from this article.

Exponential Fog Formula

Exponential Fog Formula

Exponential Result

Exponential Fog; fog factor function

We can clearly see that exponential fog has a fast decrease in density than the linear fog. I usually go with very small numbers for fog density, here I used 0.05.

Exponential squared fog is the last of the standard three fog equations. The only difference from the exponential fog is that the distance and the fog desnity are squared. Beside the incident light, this equation takes into consideration the reflected light which will create a clearer visibility around the view camera where the distance is very small followed by a fast density increase as we get further. The fog density you choose should be very small.

 Exponential Squared Fog

Exponential Squared Fog Formula

Exponential Square Fog

Exponential Square Fog; fog factor function

You can see in the upper left corner of those images that I included a function used to get the final color. There is a mix function in GLSL which blends the original color with fog color in the same way that function works. Remember that it is very important how you pass those arguments to this function. This mix function is the same for all equation.

//formula
//finalColor = (1.0 - f)*fogColor + f * lightColor

finalColor = mix(fogColor, lightColor, fogFactor);

//change fogFactor if lightColor is the first argument
finalColor = mix(lightColor, fogColor, 1.0 - fogFactor);

 Getting the distance: Z-Depth vs Length

There are different ways to get the distance(d) needed for the equations above. There is the plane-based technique where the z-depth is used to get the distance. This can cause visual anomalies when you rotate the camera. Imagine on object that is not affected by the fog in your view space, when you rotate the view camera the z changes thus the object can be affected by the fog. We can use this tehnique in certain situations when the view camera doesn’t move that much or is static. Also this is very cheap to compute so it can be used on mobile.

Plane-based Fog

Plane-based Fog


dist = abs(VP.z); //where VP is a vec4 var computed in vertex shader
                  //to get vertex position;
                  // VP = View*Model* vec4(in_position,1)
//other way
dist = gl_FragCoord.z / gl_FragCoord.w; // dependence to current camera gl_Position
                                         //works only in fragment shader

To avoid this unwanted effect with objects that leaves and enters the fog area we can use the range-based technique where we compute the actual (exact) distance between the vertex and and view camera. It is more expensive because it needs to compute a square root but on modern hardware I would not worry that much.

Range Based Fog

To see the differences we can use 1.0 – fogFactor value for RGB final color just to see how depth behaves. This is very similar to depth buffer. Black is near and white is far this is the reason why we subtract the fogFactor from 1.0.

 //show fogFactor depth(gray levels); similar to depth buffer
 fogFactor = 1 - fogFactor;
 out_color = vec4( fogFactor, fogFactor, fogFactor,1.0 );
Planar fog z depth view

Plane-based fog; fogFactor as RGB; view from above

Range base fog

Range-base fog; fogFactor as RGB; view from above

Plane-based fog vs  Range-based fog

Plane-based fog vs Range-based fog

Fog in vertex shader vs Fog in fragment shader

Now that you know the standard equations for fog and you also know how to get the distance but you may be wondering where to compute it in vertex shader or in fragment shader? Well the answer depends on how you want your fog to be interpolated. You can compute the distance and fogFactor in vertex shader, and pass it to fragment shader and the color is going to be interpolated over the primitive (in my case triangles). This is cheaper to compute and if you have high poly models, you are not able to see huge differences. However if you have a flat terrain like mine with only four vertices at a considerable distance from one each other you will get bad visual results with range-based fog. To compute your fog in the fragment shader you have to pass the viewSpace vector from the vertex shader and compute the distance and fog equations in your fragment shader. Let’s look at some differences for linear fog with range-based distance:

Linear fog Vertex

Range-based linear fog in vertex shader

Linear fog in fragment shader

Range-based linear fog in fragment shader

Now let’s look at the code for vertex and fragment shaders. To show all these images I wrote a complex shader with lots of if statements. Here I will show how to create the fragment fog to simplfy the code. However the full shader is in the source code below.

//********************
// fog vertex shader
//*******************
#version 330

layout(location = 0) in vec3 in_position;
layout(location = 1) in vec3 in_normal;
layout(location = 2) in vec2 in_texcoord;

uniform mat4 model_matrix, view_matrix, projection_matrix;

out vec3 world_pos;
out vec3 world_normal;
out vec2 texcoord;
out vec4 viewSpace;

void main(){

//used for lighting models
world_pos = (model_matrix * vec4(in_position,1)).xyz;
world_normal = normalize(mat3(model_matrix) * in_normal);
texcoord = in_texcoord;

//send it to fragment shader
viewSpace = view_matrix * model_matrix * vec4(in_position,1);
gl_Position = projection_matrix * viewSpace;

}


//fog fragment shader
//................!!!.......................
//if you decided how to compute fog distance
//and you want to use only one fog equation
//you don't have to use those if statements
//Here is a tutorial and I want to show
//different possibilities
//.........................................
#version 330
layout(location = 0) out vec4 out_color;

uniform vec3 light_position;
uniform vec3 eye_position;

uniform sampler2D texture1;

//0 linear; 1 exponential; 2 exponential square
uniform int fogSelector;
//0 plane based; 1 range based
uniform int depthFog;

//can pass them as uniforms
const vec3 DiffuseLight = vec3(0.15, 0.05, 0.0);
const vec3 RimColor = vec3(0.2, 0.2, 0.2);

//from vertex shader
in vec3 world_pos;
in vec3 world_normal;
in vec4 viewSpace;
in vec2 texcoord;

const vec3 fogColor = vec3(0.5, 0.5,0.5);
const float FogDensity = 0.05;

void main(){

vec3 tex1 = texture(texture1, texcoord).rgb;

//get light an view directions
vec3 L = normalize( light_position - world_pos);
vec3 V = normalize( eye_position - world_pos);

//diffuse lighting
vec3 diffuse = DiffuseLight * max(0, dot(L,world_normal));

//rim lighting
float rim = 1 - max(dot(V, world_normal), 0.0);
rim = smoothstep(0.6, 1.0, rim);
vec3 finalRim = RimColor * vec3(rim, rim, rim);
//get all lights and texture
vec3 lightColor = finalRim + diffuse + tex1;

vec3 finalColor = vec3(0, 0, 0);

//distance
float dist = 0;
float fogFactor = 0;

//compute distance used in fog equations
if(depthFog == 0)//select plane based vs range based
{
  //plane based
  dist = abs(viewSpace.z);
  //dist = (gl_FragCoord.z / gl_FragCoord.w);
}
else
{
   //range based
   dist = length(viewSpace);
}

if(fogSelector == 0)//linear fog
{
   // 20 - fog starts; 80 - fog ends
   fogFactor = (80 - dist)/(80 - 20);
   fogFactor = clamp( fogFactor, 0.0, 1.0 );

   //if you inverse color in glsl mix function you have to
   //put 1.0 - fogFactor
   finalColor = mix(fogColor, lightColor, fogFactor);
}
else if( fogSelector == 1)// exponential fog
{
    fogFactor = 1.0 /exp(dist * FogDensity);
    fogFactor = clamp( fogFactor, 0.0, 1.0 );

    // mix function fogColor⋅(1−fogFactor) + lightColor⋅fogFactor
    finalColor = mix(fogColor, lightColor, fogFactor);
}
else if( fogSelector == 2)
{
   fogFactor = 1.0 /exp( (dist * FogDensity)* (dist * FogDensity));
   fogFactor = clamp( fogFactor, 0.0, 1.0 );

   finalColor = mix(fogColor, lightColor, fogFactor);
}

//show fogFactor depth(gray levels)
//fogFactor = 1 - fogFactor;
//out_color = vec4( fogFactor, fogFactor, fogFactor,1.0 );
out_color = vec4(finalColor, 1);

}

Beautiful fog with atmospheric effects

We saw that exponential fog is more physical based and we used a simplified model from our real world. Here we can go further and add more complexity to the fog model. The interaction between light and medium is basically described with three main processes:

  • Emission
  • Absorption
  • Scattering

We can consider a volume (cloud) of particles that emit, absorb and scatter the light rays. Emission is contributing with energy from luminous particles. When light is absorbed the energy is converted into another energy, like heat loosing intensity. Scattering redirects the original direction of the light in other directions when the light hits a particle, loosing intensity. This is called out-scattering. If we combine the absorption effects with out-scattering effects we get the extinction (attenuation) effect. But the bounced light will hit another particle which will increase that intensity and maybe re-emit light again. This is called in-scattering. Here is an image with all light interaction effects discussed above:

TransmissionThere are more complex theories that you can read about light and transmittance with lots and lots of math and physics. Here we will again simplify this model to write a simple fog shader with just a cool effect. Remember that in “standard” fog equations we had only a single fog factor used to get our final color. We will use the exponential fog formula but this time lets use two fog factors with different density (b1 and b2) values. One fog factor is extinction and the other one is in-scattering.

scattering fogWe can go further and express b1 and b2 based on height and create that ground looking fog. This way fog density changes with altitude. However because I have only a flat grond with some trees my scene is not the best to show off. There are a couple of ways to add height to your fog, the easiest way is subtract the current vertex height (viewSpace.y) from view camera height and multiply by a small factor .

ground fog

Ground Fog


//fog fragment shader

#version 330
layout(location = 0) out vec4 out_color;

uniform vec3 light_position;
uniform vec3 eye_position;

uniform sampler2D texture1;

//0 linear; 1 exponential; 2 exponential square
uniform int fogSelector;
//0 plane based; 1 range based
uniform int depthFog;

//can pass them as uniforms
const vec3 DiffuseLight = vec3(0.15, 0.05, 0.0);
const vec3 RimColor = vec3(0.2, 0.2, 0.2);

//from vertex shader
in vec3 world_pos;
in vec3 world_normal;
in vec4 viewSpace;
in vec2 texcoord;

const vec3 fogColor = vec3(0.5, 0.5,0.5);

void main(){

vec3 tex1 = texture(texture1, texcoord).rgb;

//get light an view directions
vec3 L = normalize( light_position - world_pos);
vec3 V = normalize( eye_position - world_pos);

//diffuse lighting
vec3 diffuse = DiffuseLight * max(0, dot(L,world_normal));

//rim lighting
float rim = 1 - max(dot(V, world_normal), 0.0);
rim = smoothstep(0.6, 1.0, rim);
vec3 finalRim = RimColor * vec3(rim, rim, rim);
//get all lights and texture
vec3 lightColor = finalRim + diffuse + tex1;

vec3 finalColor = vec3(0, 0, 0);

//compute range based distance
float dist = length(viewSpace);

//my camera y is 10.0. you can change it or pass it as a uniform
float be = (10.0 - viewSpace.y) * 0.004;//0.004 is just a factor; change it if you want
float bi = (10.0 - viewSpace.y) * 0.001;//0.001 is just a factor; change it if you want

//OpenGL SuperBible 6th edition uses a smoothstep function to get
//a nice cutoff here
//You have to tweak this values
// float be = 0.025 * smoothstep(0.0, 6.0, 32.0 - viewSpace.y);
// float bi = 0.075* smoothstep(0.0, 80, 10.0 - viewSpace.y);

float ext = exp(-dist * be);
float insc = exp(-dist * bi);

finalColor = lightColor * ext + fogColor * (1 - insc);

out_color = vec4(finalColor, 1);

}

Source Code OpenGL 3.3 Visual Studio 2013 :Fog OpenGL

Watch console to change keyboard keys in order to see different types of fog.

 Further reading:

Better Fog: http://www.iquilezles.org/www/articles/fog/fog.htm

Crytek: Real Time Atmospheric Effects in Games

Scratchapixel.com: Atmospheric Scattering

GPU Gems2 : Accurate Athmospheric Scattering

Gamasutra : Volumetric Rendering in RealTime

Books: ShaderX ^2, OpenGL SuperBible 6th Edition


blog comments powered by Disqus