Saturday, May 25, 2013

Animated Shaders in Three.js

Once again I have been investigating Shaders in Three.js.  My last post on this topic was inspired by glow effects; this time, my inspiration comes from the Three.js lava shader and Altered Qualia's WebGL shader fireball.


For these examples, I'm really only interested in changing the pixel colors, so I'll just worry about the fragment shader.

The Three.js (v.56) code  for the a simple fragment shader that just displays a texture would be:
<script id="fragmentShader" type="x-shader/x-vertex">
uniform sampler2D texture1;
varying vec2 vUv;
void main() 
{
    vec4 baseColor  = texture2D( texture1, vUv );
    gl_FragColor = baseColor;
} 
</script>

(note that this code is contained within its own script tags.)

Then to create the material in the initialization of the Three.js code:
var lavaTexture = new THREE.ImageUtils.loadTexture( 'images/lava.jpg' );
lavaTexture.wrapS = lavaTexture.wrapT = THREE.RepeatWrapping; 

var customUniforms = { texture1: { type: "t", value: lavaTexture }   };

// create custom material from the shader code above within specially labeled script tags
var customMaterial = new THREE.ShaderMaterial( 
{
    uniforms: customUniforms,
    vertexShader:   document.getElementById( 'vertexShader'   ).textContent,
    fragmentShader: document.getElementById( 'fragmentShader' ).textContent
}   );

What we would like to create a more realistic effect is to:
  • add a bit of "random noise" to the texture coordinates vector (vUv) to cause distortion
  • change the values of the random noise so that the distortions in the image change fluidly
  • (optional) add support for transparency (custom alpha values)
To accomplish this, we can add some additional parameters to the shader, namely:
  • a second texture (noiseTexture) to generate "noise values"; for instance, we can add the red/green values at a given pixel to the x and y coordinates of vUv, causing an offset
  • a float (noiseScale) to scale the effects of the "noise values"  
  • a float (time) to pass a "time" value to the shader to continuously shift the texture used to generate noise values
  • a float (baseSpeed) to scale the effects of the time to either speed up or slow down rate of distortions in the base texture
  • a float (alpha) to set the transparency amount
The new version of the fragment shader code looks like this:
<script id="fragmentShader" type="x-shader/x-vertex"> 
uniform sampler2D baseTexture;
uniform float baseSpeed;
uniform sampler2D noiseTexture;
uniform float noiseScale;
uniform float alpha;
uniform float time;
varying vec2 vUv;

void main() 
{
    vec2 uvTimeShift = vUv + vec2( -0.7, 1.5 ) * time * baseSpeed;
    vec4 noise = texture2D( noiseTexture, uvTimeShift );
    vec2 uvNoisyTimeShift = vUv + noiseScale * vec2( noise.r, noise.g );
    vec4 baseColor = texture2D( baseTexture, uvNoisyTimeShift );
    baseColor.a = alpha;
    gl_FragColor = baseColor;
}
</script>

The new code to create this shader in Three.js:
// initialize a global clock to keep track of time
this.clock = new THREE.Clock();

var noiseTexture = new THREE.ImageUtils.loadTexture( 'images/noise.jpg' );
noiseTexture.wrapS = noiseTexture.wrapT = THREE.RepeatWrapping;

var lavaTexture = new THREE.ImageUtils.loadTexture( 'images/lava.jpg' );
lavaTexture.wrapS = lavaTexture.wrapT = THREE.RepeatWrapping; 

// create global uniforms object so its accessible in update method
this.customUniforms = {
    baseTexture:  { type: "t", value: lavaTexture },
    baseSpeed:    { type: "f", value: 0.05 },
    noiseTexture: { type: "t", value: noiseTexture },
    noiseScale:   { type: "f", value: 0.5337 },
    alpha:        { type: "f", value: 1.0 },
    time:         { type: "f", value: 1.0 }
}

// create custom material from the shader code above within specially labeled script tags
var customMaterial = new THREE.ShaderMaterial( 
{
    uniforms: customUniforms,
    vertexShader:   document.getElementById( 'vertexShader'   ).textContent,
    fragmentShader: document.getElementById( 'fragmentShader' ).textContent
}   );

and in the update or render method, don't forget to update the time using something like:
var delta = clock.getDelta();
customUniforms.time.value += delta;

That's about it -- for a live example, check out the demo in my GitHub collection, which uses this shader to create an animated lava-style texture and animated water-style texture.


Happy coding!