Update

Following the recent changes in Three.js rev50/dev, the godrays shader provided on my github now uses unflipped v coordinates. If you use a Three.js version pre r50/dev, use the previous flipped version.

This change comes from the recent choice in the development of Three.js to ditch out the v coordinate flip on textures (see r50/dev changelog).

Note that this update only affects the shader code on github, while the demo still runs on Three.js r48/dev. Only my next demos will feature r50/dev.

Demo

Volumetric light approximation (Godrays) Three.js demo (best viewed in Chrome or Firefox).

Demo source on Github.

Video

Occlusion buffer

In order to get this light shaft effect we will need an occlusion buffer. This can be achieved by rendering the light as a colored sphere, while rendering every other objects as black using depth testing.

There are two ways to do so in OpenGL. The first one is to use Multiple Render Targets (MRT) to output the occlusion frame into a separate buffer on render time. Sadly, MRT is not available in WebGL (OpenGL ES 2.0), so we'll have to render the scene twice to get our main buffer and the occlusion buffer.

To render and work with these two buffers separatly we'll be using THREE.EffectComposer, the same way I did with my previous glow rendering tutorial.

Preparing the objects

To render our scene with the black materials and the light we need to create a separate scene, with the same objects.

Therefore we need to initialise two scenes, one for the main render, and one for the occlusion pass.

// MAIN SCENE
var camera = new THREE.PerspectiveCamera( 75, SCREEN_WIDTH / SCREEN_HEIGHT, 1, 100000 );
var scene = new THREE.Scene();
scene.add( new THREE.AmbientLight( 0xffffff ) );
var pointLight = new THREE.PointLight( 0xffffff );
pointLight.position.set( 0, 100, 0 );
scene.add( pointLight );

// OCCLUSION SCENE
var oclscene = new THREE.Scene();
oclscene.add( new THREE.AmbientLight( 0xffffff ) );
oclcamera = new THREE.PerspectiveCamera( 75, SCREEN_WIDTH / SCREEN_HEIGHT, 1, 100000 );
oclcamera.position = camera.position;

Now adding the volumetric light representation to the occlusion scene, I chose to use a simple isocahedron (using a gradient billboard can provide more realistic results).

// Volumetric light
vlight = new THREE.Mesh( 
	new THREE.IcosahedronGeometry(50, 3),
	new THREE.MeshBasicMaterial({
		color: 0xffffff
	})
);
vlight.position.y = 0;
oclscene.add( vlight );

When you add your 3D object to the scene, you will have to add a cloned version of that object with a black MeshBasicMaterial.

function createScene( geometry, x, y, z, b ) {

	// Base object
	zmesh = new THREE.Mesh( geometry, new THREE.MeshFaceMaterial() );
	zmesh.position.set( x, y, z );
	zmesh.scale.set( 3, 3, 3 );
	scene.add( zmesh );

	// Occluding object
	var gmat = new THREE.MeshBasicMaterial( { color: 0x000000, map: null } );
	var geometryClone = THREE.GeometryUtils.clone( geometry );
	var gmesh = new THREE.Mesh(geometryClone, gmat);
	gmesh.position = zmesh.position;
	gmesh.rotation = zmesh.rotation;
	gmesh.scale = zmesh.scale;
	oclscene.add(gmesh);
}

Applying a radial blur to the occlusion buffer

In order to get this light shafts effect, we will then need to apply a parametrised radial blur to the occlusion buffer after bluring it (for quality and smoothness).

These steps will be done using THREE.EffectComposer and THREE.ShaderExtras.

The main radial blur algorithm for the godray fragment shader can be found in my Three.js extras repository on github.

We need an EffectComposer to render the occlusion scene and apply the blurs on it. Take note of the half size for the effect buffer (SCREEN_WIDTH/2), this makes the overall rendering much faster.

// Prepare the occlusion composer's render target
var renderTargetParameters = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBFormat, stencilBufer: false };
renderTargetOcl = new THREE.WebGLRenderTarget( SCREEN_WIDTH/2, SCREEN_HEIGHT/2, renderTargetParameters );

// Prepare the simple blur shader passes
hblur = new THREE.ShaderPass( THREE.ShaderExtras[ "horizontalBlur" ] );
vblur = new THREE.ShaderPass( THREE.ShaderExtras[ "verticalBlur" ] );

var bluriness = 3;

hblur.uniforms[ "h" ].value = bluriness / SCREEN_WIDTH;
vblur.uniforms[ "v" ].value = bluriness / SCREEN_HEIGHT;

// Prepare the occlusion scene render pass
var renderModelOcl = new THREE.RenderPass( oclscene, oclcamera );

// Prepare the godray shader pass
var grPass = new THREE.ShaderPass( THREE.Extras.Shaders.Godrays );
grPass.needsSwap = true;

// Prepare the composer
var oclcomposer = new THREE.EffectComposer( renderer, renderTargetOcl );
oclcomposer.addPass( renderModelOcl );
oclcomposer.addPass( hblur );
oclcomposer.addPass( vblur );
oclcomposer.addPass( hblur );
oclcomposer.addPass( vblur );
oclcomposer.addPass( grPass );

That way we will be able to use oclcomposer.render() in our render loop to render the occlusion buffer and the different blur effects.

The godray shader

This effect is based on an article by Kenny Mitchell from EA.

This shader requires the following uniforms :

{
	tDiffuse: {type: "t", value:0, texture:null},
	fX: {type: "f", value: 0.5},
	fY: {type: "f", value: 0.5},
	fExposure: {type: "f", value: 0.6},
	fDecay: {type: "f", value: 0.93},
	fDensity: {type: "f", value: 0.96},
	fWeight: {type: "f", value: 0.4},
	fClamp: {type: "f", value: 1.0}
}

  • tDiffuse is the occlusion buffer sampler2D.
  • fX and fY are the screen coordinates of the light (see below) and will serve as center for the radial blur.
  • The other uniforms control the intensity and density of the effect (fiddle with the demo to see their impact).

In order to get the screen-space light coordinates, we will have to project the light position using our camera. This can be done easily using the projectOnScreen method in my THREE.Extras.Utils plugin:

var pos = THREE.Extras.Utils.projectOnScreen(lightMesh, camera);

Note that we will have to update the godrays shader's uniforms accordingly in the render loop:

grPass.uniforms.fX.value = pos.x;
grPass.uniforms.fY.value = pos.y;

The godrays shader will then apply a standard radial blur to the occlusion buffer.

Blending the effect buffer to the base scene

For this we will be using the THREE.Extras.Shaders.Additive shader.

var finalPass = new THREE.ShaderPass( THREE.Extras.Shaders.Additive );
finalPass.needsSwap = true;
finalPass.uniforms.tAdd.texture = oclcomposer.renderTarget1;

Next step is to render the base scene using another RenderPass and blend it with the occlusion/godrays buffer using another EffectComposer.

// Prepare the base scene render pass
var renderModel = new THREE.RenderPass( scene, camera );

// Prepare the additive blending pass
var finalPass = new THREE.ShaderPass( finalshader );

// Make sure the additive blending is rendered to the screen (since it's the last pass)
finalPass.renderToScreen = true;

// Prepare the composer's render target
renderTarget = new THREE.WebGLRenderTarget( SCREEN_WIDTH, SCREEN_HEIGHT, renderTargetParameters );

// Create the composer
finalcomposer = new THREE.EffectComposer( renderer, renderTarget );

// Add all passes
finalcomposer.addPass( renderModel );
finalcomposer.addPass( finalPass );

Render everything

Finally, the last step is to call the two composers' render method in the main render loop.

function animate() {
	requestAnimationFrame( animate );
	render();
}

function render() {
	// ... move camera and light as needed

	// Update light position uniforms
	var pos = THREE.Extras.Utils.projectOnScreen(vlight, camera);
	grPass.uniforms.fX.value = pos.x;
	grPass.uniforms.fY.value = pos.y;

	// Render
	oclcomposer.render();
	finalcomposer.render();
}

Links