Making a noise (film grain) post-processing effect from scratch in ThreeJs

The tutorial for after the tutorial

This is once again the tutorial for after you have learned the basics and want to learn a bit more. The initial foci of this tutorial are pseudo random number generation as well as passing a uniform to a post processing shader.

If you are new, you can see CJGammons tutorial which is a great intro, right here: linkylinklink

Setting up the scene

Above I have provided the scene we will be starting with. It is 4 simple spheres with lambert materials and some lights to make them look half decent. The camera slowly circles them like some strange high functioning vulture but the lighting says the same.

I have also done a crucial part... set up the EffectComposer, added a render pass and added my custom shader, adding the tDiffuse uniform as necessary. If you dont understand how or why we do this i refer you back to the link earlier in the article.

What is film grain

According to wikipedia: Film grain or granularity is the random optical texture of processed photographic film due to the presence of small particles of a metallic silver, or dye clouds, developed from silver halide that have received enough photons.

As you can see from the image on this left this isn't a pure random distribution of noise but you can get a similar effect that is computationally much cheaper. This tutorial is also more for learning that accuracy

Function for noise

The fragment shader is running for every fragment, which for a post processed render pass is the x/y of the pixel. As there is no actual random number function in GLSL I pulled a great way to fake it from the book of shaders:

float random( vec2 p )
{
  vec2 K1 = vec2(
  23.14069263277926, // e^pi (Gelfond's constant)
  2.665144142690225 // 2^sqrt(2) (Gelfond–Schneider constant)
);
  return fract( cos( dot(p,K1) ) * 12345.6789 );
}

Using a vector of two constants, taking the dot product of that and the supplied vector, the cosine of that then the fract of THAT and we have a pseudo-random number.

My original approach was to pass the uv coordinate as the random number however this makes it so the noise created stayed the same based on its location and I wanted the 'crackleiness' of film. For this we would have to provide the GPU with a number that changes. We would need to pass in a uniform.

Passing in a uniform

Uniforms are declared in the constructor for the ShaderPass, and are passed in as an object, like below

var myEffect = {
  uniforms: {
    "tDiffuse": { value: null },
    "amount": { value: counter }
  },
  vertexShader: vertShader,
  fragmentShader: fragShader
}

These can be then changed once per frame, giving the random number generation a different seed every frame and therefore create an updating noise. The uniforms are accessed directly from the shaderpass object like so:

//inside my render() function
counter += 0.01;
customPass.uniforms["amount"].value = counter;

Setting up the noise function

As the 'amount' uniform is the same for every vertex, if we ran it through our pseudo-random function then each point would generate the same value. I found the best thing to do in this case was to take the current y location of the fragment, use that and the 'amount' uniform to create a random number.

We can then add that to the overall color of the fragment by adding it to the r, g and b. We can also use a multiplication to control the graininess. I like the grain to be more subtle so I used a small multiplier of 0.1

That theory looks like this in our fragment shader:

uniform float amount;
uniform sampler2D tDiffuse;
varying vec2 vUv;

float random( vec2 p )
{
  vec2 K1 = vec2(
    23.14069263277926, // e^pi (Gelfond's constant)
    2.665144142690225 // 2^sqrt(2) (Gelfond–Schneider constant)
  );
return fract( cos( dot(p,K1) ) * 12345.6789 );
}

void main() {
  vec4 color = texture2D( tDiffuse, vUv );
  vec2 uvRandom = vUv;
  uvRandom.y *= random(vec2(uvRandom.y,amount));
  color.rgb += random(uvRandom)*0.15;
  gl_FragColor = vec4( color );
}

Example code on codepen