Three.js Fog Hacks

Fog is everything. Its a simple effect that effortlessly adds depth and cinematic qualities to a scene. It’s also has negligible performance costs, meaning it’s suitable for web graphics. Imagine a cube on a plane with a sky sphere.. seems pretty bland but then add FOG and BOOM! Masterpiece.

3D mountains without fog 3D mountains with fog
this is the.. same scene… whaat?

I think most 3D designers/nerds/whatever are pretty aware of the glories of fog, but from what i’ve seen, the effect is normally just taken at face value. The three.js default fog is definitely beautiful on its own, but I want to share some shader simple tricks on how to take the implementation to the ~ next level ~.

I’m going to be doing this exploration in three.js, but the basic ideas can be easily implemented in shaders everywhere. Also, if you are the kind of person that hates reading and is screaming “JUST SHOW ME THE CODE”, the demo and code is available @ https://stackblitz.com/edit/threejs-fog-hacks

···

So, let’s begin with the three.js fog demo as the starting point. This is the only mention of fog on the three.js examples page, yet it seems to be more about the terrain :(

The article requires a basic understanding of how the shader library works in three.js. If you are not familiar, I would really recommend reading this article on Extending Three.js Materials , its good stuff.

Also, i’m going to mostly just go over concepts, with code snippets here and there, so I advise that you have the full Stackblitz demo code open for reference :)

THE SETUP

So, our first goal is to get to a hold of the fog implementation and have it under our control. One way to do that would be writing a custom shader from scratch, but this would mean having to reimplement a lot of the material features ( present in THREE.MeshBasicMaterial, THREE.MeshPhysicalMaterial, etc. ) that you are already love and are using for your scene.

You could also just copy and paste the bones of the material shaders you are using, and overwrite the fog shader chunks with your own.. This is better, but since it is still a custom shader and of type THREE.ShaderMaterial , it will miss out on the integration with the rest of the scene properties ( including lights and fog).

For example, the standard way to control fog is scene.fog = new THREE.FogExp2(color, density) . For all built in materials, the fog parameters are automatically passed in by the library as uniforms with the correct shader #defines. However, they are ignored if the material is of type Three.ShaderMaterial .

So… what we need is a material that is a default type but also has our custom logic?? Don’t worry, there’s a third option! We can instantiate our material as whatever base material we want (so that it is registered and gets all scene properties), and just sneak in the chunks that we want to replace in the material OnBeforeCompile callback.

mesh.material = new THREE.MeshBasicMaterial({ color: new THREE.Color(0xefd1b5) }); mesh.material.onBeforeCompile = (shader) => { shader.vertexShader = shader.vertexShader.replace(`#include <fog_pars_vertex>`, fogParsVert); shader.vertexShader = shader.vertexShader.replace(`#include <fog_vertex>`, fogVert); shader.fragmentShader = shader.fragmentShader.replace(`#include <fog_pars_fragment>`, fogParsFrag); shader.fragmentShader = shader.fragmentShader.replace(`#include <fog_fragment>`, fogFrag); };

Yay! Okay, but let’s take a step back… how do we know which chunks we need to replace?

By studying the shader chunks on Github. No but really, the only way we can successfully use this approach is by getting familiar with what the included shader chunks do, what #defines exist, what varyings are provided, etc.

I recommend two things:

  1. Go to Github and look at the shaders chunks that compose each material, and dive into the shader chunks you are interested in.
  2. If you “break” the shader by, lets say, writing an invalid line like hellofdksaf #include <fog_fragment>: you will get an error message in the console with the full print out of the shader and all its chunks in one place! it’s nice reading material.

I feel like the setup is actually one of the coolest parts of this lil article, since it sets you up with the freedom to pretty much hack any part of the three.js shader system with little resistance.

Once you’ve done those things, you will see that the fog uniforms are presented in <fog_pars_vertex> & <fog_pars_fragment>, and the implementing is in <fog_vertex>, <fog_fragment>. You will want to start out with the default implementation in a javascript file , that we can slowly modify.

NEAR + APEX COLORS

Finally ready for our first hack! Who said fog had to be restricted to one color?? It may not be physically accurate, but it surely does give an artistic edge to be able to control fog colors that are near and at a distance.

At the end of the fog fragment chunk, there is a float named fogFactor , a 0-1 value that represents where the fragments depth value lies on the scale of no fog, to full fog. It is used to calculate the final fragment color like so:

gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );

When fogFactor is 0, the color remains equal to gl_FragColor. When fogFactor is 1, the fragment is at our max depth value and is fully in fog.

We can achieve a dual fog color system by having fogColor depend on the fogFactor as well. If a new uniform fogNearColor is the color of fragments closer to you, and fogColor is for those far away, we can mix the two depending on the fogFactor:

gl_FragColor.rgb = mix ( gl_FragColor.rgb, mix (fogNearColor, fogColor, fogFactor), fogFactor );
Green fog over green 3D mountains Blue fog over red 3D mountains
Turquoise fog over purple 3D mountains Dark blue fog over white-bluish 3D mountains
Few looks with different near and apex colors

Of course this can also be extended to having horizon + sky fog colors, but thats something for you to try. Moving on.

NOISE

The three.js fog is fixed at every depth value, meaning the fog will look the same in every direction. From the example above it’s not too noticeable since the terrain we are working with is complex on its own, but if you have simpler geo, it becomes pretty obvious quickly.

Real fog has patches, areas that are denser than others. We can try to mimics that complexity by applying a noise function to the depth value.

Perlin noise will solve all your problems. -me

There is a cool resource of GLSL Noise algorithms that I tend to include in all of my three.js project, before even starting to work on the shaders, and they always come in handy.

For the purpose of fog, we are going to need a noise function that takes a 3D input, since we want it to be fixed and driven by the world space position. I’m going to go with Classic Perlin 3D Noise by Stefan Gustavson, but its fun to experiment with others if you want :)

The first thing we are going to need is to set up the world position value as a varying, to access it in our fragment shader. That includes changes in all of our replacement chunks : Declaring vFogWorldPosition in the headers, setting it in the vertex shader chunk vFogWorldPosition = (modelMatrix * vec4( transformed, 1.0 )).xyz; , and using it in the fragment shader. Another note to look at the stackblitz if you need more details.

In the fragment shader, let’s just start and see what the noise looks like by multiplying it with the fogDepth value. Since cnoise outputs a value between 0–1, this should be a kosher experiment.

float noise = cnoise(fogNoiseFreq * vFogWorldPosition.xyz); float vFogDepth = noise * fogDepth;
fogNoiseFreq value of 0.0036 fogNoiseFreq value of 0.0012
fogNoiseFreq of 0.0036 & 0.0012

Playing around with different values of fogNoiseFreq gives us some pretty interesting looks already! But since we are multiplying all fogDepth values equally by noise, we are losing the constant gradual shift to complete fog.

Scaling the noise by the ogDepth value, as well as adding a multiplier for artistic taste should help us maintain that gradual shift.

float vFogDepth = fogDepth - fogNoiseImpact * noise * fogDepth;
Fog with Perlin noise
Fog with Perlin noise

MOVEMENT

Ok maybe i’ve been staring at the fog too much ( can’t help it in Daly City.. ) , but fog is rarely static here. There’s always a gust or slow wind pushing it ( karl ) along its way.

In order to start making the fog move, our shader needs a time uniform ( reference codeif you are unsure of how to add that ). Then, all we need to do is add a direction to vFogWorldPosition, and the noise will smoothly transition with time.

vec3 windDir = vec3(0.0, 0.0, time); vec3 scrollingPos = vFogWorldPosition.xyz + fogNoiseSpeed * windDir; float noise = cnoise(fogNoiseFreq * scrollingPos.xyz);
Moving Perlin noise over mountains
Moving Perlin noise over mountains. Banding is shitty :( Pls check out the demo for a better idea.

PERFORMANCE

Having the noise function in the fragment shader looks awesome, but isn’t the most performative option. If you are tied on performance, there is always the option of losing some smoothness and calculating the noise in the vertex shader.

If you are looking for a more subtle effect, you can create a pretty similar effect by approximating with parametric surfaces.

3D plot of parametric surface
3D plot of parametric surface

I think I was browsing parametric surfaces for something else.. but when I saw this plot I was like ooooh looks like a blanket of fog!! Again, thats what living in Daly City does to you…

We can use the x & z values of our world position to calculate the surface, and then do a fade base on how close the y position is to the surface value — that way basically any point under the surface would have fog, and any point over would smoothly transition to no fog.

float scale = 0.04; float yCutoff = 3.0; vec3 scaledPos = scale * vWorldPosition.xyz; float val = scaledPos.z * sin(scaledPos.x) - scaledPos.x * cos(scaledPos.z); float normDist = clamp(vWorldPos.y - yCutoff*val, 0.0, 100.0)/100.0;

There is some hackery you need to do if you want to add movement to this fog, since this particular function does not look good when x & y move away from the origin.. I solved this by just moving the world position in a circular pattern, but i’m sure there are other ways :)

Parametric surface fog approximation for one of my projects. Subtle, but trust me, its there :)
Parametric surface fog approximation for one of my projects. Subtle, but trust me, its there :)

FIN

Anyways, I hope there were bits and pieces of that that would be useful to some of y’all threejs friends out theree! Feel free to reach out if anything was unclear, or if you come up with some other cool hacks :)

❤ snayz

Resources

Final demo @ https://threejs-fog-hacks.stackblitz.io/

Check out other related repos @ https://github.com/sneha-belkhale

Other VR/Graphics projects @ https://codercat.tk

And renders / screenshots @ https://www.instagram.com/snayss/