Lesson 36 - Shader Glow and Effect Composer

 

Introduction

This lesson is a little different than some of the others as the original NeHe demo wasn't too interesting and used features in OpenGL that aren't exposed in WebGL. So this lesson followis the original in spirit rather than to the letter. The original (apparently) faked a radial blur. Apparently, because few if any of the original NeHe demos can be built and run anymore as-is since they rely on obsolete tools and libraries.

So instead this lesson creates a halo effect using the shader. It also uses the effect composer to composite several different passes.

Initialization

First step is to create the torus know which will be the geometry.

var torusGeom = new THREE.TorusKnotGeometry(2.0, 0.5, 100, 16);
var torusMat = new THREE.MeshPhongMaterial({ color: '#cccccc', specular: '#111111', shininess: 50 });
knot = new THREE.Mesh(torusGeom, torusMat);
gfxScene.add(knot);

Then we create another torus knot that will be the "halo":

var haloColor = new THREE.Vector3(212 / 255.0, 175 / 255.0, 55 / 255.0);
var haloMat = new THREE.ShaderMaterial({
    uniforms: { vColor: { type: "v3", value: haloColor } },
    vertexShader:   document.getElementById( 'vertexShader'   ).textContent,
    fragmentShader: document.getElementById( 'fragmentShader' ).textContent,
    side: THREE.BackSide,
    blending: THREE.AdditiveBlending,
    transparent: true
});

var haloScale = 1.1;
var haloGeom = new THREE.TorusKnotGeometry(2*haloScale, 0.5*haloScale, 100, 16);
halo = new THREE.Mesh( haloGeom, haloMat );
gfxScene.add( halo );

Note that the halo know if 10% bigger than the first one (else it wouldn't be a halo, eh?). The material is of course a shader-material. The only uniform we need to pass to the shader is the color of the halo (gold). The halo will be partially transparent so we set the transparent propety to true and set blending to additive. Also note that we tell three.js to render only the backside of the object. If we render on the front or both sides of halo-knot we get weird artifacts.

Effects Composer

Next we set up the EffectsComposer. The three.js EffectComposer is a library that allows one to layer rendering "passes". Normally, one sets up the scene and simply calls render.scene() whcih renders everything in one pass. However, sometimes one wants layer various effects one on top of the other. Othertimes you want to render in more than one pass. This can be done serveral ways. In Lesson 23, we used two cameras to allow us to paint a background image on top of which we rendered the "reflected" image. We could have done that here as well, but instead we'll use the EffectComposer instead, just for fun.

The EffectComposer effectively renders to an off-screen buffer. So the sequence is to first clear the off-screen, then render the other passes, which will be rendered on top of each other in the buffer so anything in the "back" should be rendered first. So the set setup code is like this:

var clearPass = new THREE.ClearPass();

var texture = new THREE.TextureLoader().load( 'images/deathvalleysky.jpg' );
var texturePass = new THREE.TexturePass( texture );

var renderPass = new THREE.RenderPass(gfxScene.scene, gfxScene.camera);
renderPass.clear = false;

var copyPass = new THREE.ShaderPass(THREE.CopyShader);
copyPass.renderToScreen = true;
        
composer = new THREE.EffectComposer(gfxScene.renderer);

composer.addPass( clearPass );
composer.addPass( texturePass );
composer.addPass( renderPass );
composer.addPass( copyPass );

First we allocate the clearPass, which does just that - clears the offscreen buffer. Then we create the backdrop for our scene by loading an image. The resulting texture will be rendered as the background for the scene. Then a "render" pass which is the equivalent of render.scene. Note that we set the clear property to false, because rendering normally clears the buffer when it renders - which we obviously don't want. Finally, we allocate a copy pass, which effectively copies the offscreen buffer to the actual 2D screen buffer so we see the result. Then we allocate the EffectComposer and add the passes to it. Thus the sequence is:

  • Clear the offscreen buffer
  • Render the backgound texture in the offscreen buffer
  • Render the scene objects (the two tori)
  • Copy the offscreen buffer to the on-screen buffer

A final note about the EffectComposer. Our usage here is pretty trivial. The EffectComposer is a complex and poweful library. Well worth investigation. A good starter article is here.

 

The Halo

So how does the halo get generated? Just a little more shader magic. A very little. Here's the code:

<script id="vertexShader" type="x-shader/x-vertex">
    uniform vec3 uColor;
    varying vec3 vNormal;

    void main() {
        vNormal = normalize( normalMatrix * normal );
        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }
</script>

<script id="fragmentShader" type="x-shader/x-fragment">
    uniform vec3 uColor;
    varying vec3 vNormal;

    void main() {
	      float intensity = pow( 0.3 - dot( vNormal, vec3( 0.0, 0.0, 1.0 ) ), 0.5 );
        gl_FragColor = vec4( uColor, 0.5 ) * intensity;
    }
</script>

So all this is doing is calculating the normalized normal vector in the vertex shader and passing that vector to the fragment shader. In the fragment shader, it calculates the intensity of the glow as the square root of the dot product of the normal for the vertex and the Z-axis. Since the vectors are both normalized, this effectively means that it is the square root of the angle between the normal and the Z-axis. Thus the intensity is maximized in the X-Y plane, resulting in a halo around the knot.

And that's it! Click on this link to see the actual rendered demo in all it's glowing glory!

As always, the original sources are on github here.



About Us | Contact Us | ©2017 Geo-F/X