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!