FDNavigate back to the homepage

Creating Volumetric Lights with Radial Blur in Three.js Using Layers

April 15th, 2023 · 3 min read

In this tutorial Im going to quickly run through how to use radial blur to give a fake volumetric light without the need to use raymarching which would be complicated and expensive.

I tried radial blur a while ago as shown in this codesandbox.

This was the precursor to the finished volumetric sandbox.

So whats the premise?


First off we need two layers in our @react-three/fiber scene , one with the blur applied and one with everything else in. We will need a final additive shader pass which will combine these two layers.

A word of warning, this needs to be used carefully as rendering a particularly large scene twice has downsides in terms of being computationally expensive.

So with this in mind if you use this for a large scene then you will need to create some sort of level of detail (LOD) system where you would show low poly transparent light shafts at distance and only add things close to the camera into the radial blur layer.

Just food for thought. But in general this is alot cheaper than doing volumetric raymarching which would tax any mid range computer, with anything that even comes close to being complicated. I havent done ray marching in a while but doing many calculations per fragment on screen doesnt work to well on the web for the average user.

Radial Blur

What is radial blur ?

Radial blur example

Radial Blur is a type of blur which usually eminates from a central point and then affects an image outwards.

The general shader is below.

1const float exposure = 0.055;
2const float decay = .99;
3const float density = 0.54;
4const float weight = 4.75;
6// Inner used valuesa
7vec2 deltaTextCoord = vec2(vUv.xy - light_on_screen.xy);
8vec2 textCoo = vUv.st;
9deltaTextCoord *= 1.0 / float(NUM_SAMPLES) * density;
10float illuminationDecay = .10;
12vec4 c = vec4(0.0, 0.0, 0.0, 1.0);
14for(int i=0; i < NUM_SAMPLES ; i++)
16 textCoo -= deltaTextCoord;
18 textCoo.s = clamp(textCoo.s, 0.0, 1.0);
19 textCoo.t = clamp(textCoo.t, 0.0, 1.0);
21 vec4 sample1 = texture2D(tDiffuse, textCoo);
23 sample1 *= illuminationDecay * weight;
25 c += sample1;
27 illuminationDecay *= decay;
30c *= exposure;
32c.r = clamp(c.r, 0.0, 1.0);
33c.g = clamp(c.r, 0.0, 1.0);
34c.b = clamp(c.r, 0.0, 1.0);

deltaTextCoord is the different between the light source to this fragment, in 2D. We then divide by the number of samples to increase the resolution or detail of the effect. If we didnt do this then every sample we would divide by the un altered difference between the light source and this fragment, which is massive so to reduce the space between each sample or radial blur we divide the deltaTexcCoord by 1/ samples. which means we divde the difference by the samples.

c represents the color we accumulate over the different texture samples we do. The accumulation of the blur of each fragment from the origin surround the fragment we are on.

We then loop through the smaples count and minus the difference from this fragments uvs so we can blur this fragments color with the surrounding fragments.

Although we dont have state in shaders we can sample textures in neighbouring fragments to get the color of a texture for the neighbouring frgament.

The effect should be less prominate the further away you get from center, so we want the color to have a decay, so we multiply the sample by the illuminationDecay. And finally decrease the illuminationDecay by 1% each sample loop we do.

Exposure is basically how bright the effect is and the last thing we do is clamp the colors components r/g/b to 0.0-1.0, so we dont get any out of bound colors.

R3F / Three layers

The crucial part of the whole project is.. layers. This is quite a common principle in gaming where the camera will have layers and you can apply affects to certain parts of a scene and then merge both layers with an additive shader.

1useFrame((state) => {
2 state.gl.setRenderTarget(target);
3 state.gl.render(scene, camera);
4 camera.layers.set(2);
5 radialBlur.uniforms["depthTexture"].value = target.depthTexture;
6 radialBlur.uniforms["uResolution"].value = new THREE.Vector2(
7 size.width,
8 size.height
9 );
10 radialBlur.uniforms["camPos"].value = new THREE.Vector3().copy(
11 camera.position
12 );
13 radialBlur.uniforms["cameraWorldMatrix"].value = camera.matrixWorld;
14 radialBlur.uniforms[
15 "cameraProjectionMatrixInverse"
16 ].value = new THREE.Matrix4().copy(camera.projectionMatrix).invert();
18 radialBlur.uniforms["windowPos"].value = new THREE.Vector3(-2, 0, 0);
19 radialBlurComposer.render();
20 camera.layers.set(1);
21 finalComposer.render();
22}, 1);

We set the rendertarget for depth, then render the scene, set the layer to be 2 (the layer with the windows in) and then we render the radial blur shader. The set the layer to 1 which has the rest of the scene in, we pass the final composer a occlusion render target with the radiul blurred scene in. And then in this final composer we combine the two scenes.

Chaining effect composers

We use the first effect composer to render the scene and apply the radial blur, the occulsion render target stores the radial blurred image of the scene in it. This then gets passed as a uniform to the second and final effect composer, combined with the final standard tDiffuse texture which is a standard uniform of an effect composer, which stores a snap shot of whats being rendered per frame.

1const [radialBlurComposer, finalComposer, radialBlur] = useMemo(() => {
2 const renderScene = new RenderPass(scene, camera);
4 const occlusionRenderTarget = new THREE.WebGLRenderTarget(
5 window.innerWidth * 0.5,
6 window.innerHeight * 0.5
7 );
9 const radialBlurComposer = new EffectComposer(gl, occlusionRenderTarget);
10 radialBlurComposer.renderToScreen = false;
11 radialBlurComposer.addPass(renderScene);
13 const radialBlur = new ShaderPass(RadialBlurShader);
14 radialBlur.needsSwap = false;
15 radialBlur.uniforms.radialCenter.value = new THREE.Vector2(0.5, 0.5);
16 radialBlur.uniforms.intensity.value = 10;
17 radialBlur.uniforms.iterations.value = 300;
18 radialBlurComposer.addPass(radialBlur);
20 const finalComposer = new EffectComposer(gl);
21 finalComposer.addPass(new RenderPass(scene, camera));
23 const additivePass = new ShaderPass(CombiningShader());
24 additivePass.uniforms.tAdd.value = occlusionRenderTarget.texture;
25 additivePass.needsSwap = false;
26 finalComposer.addPass(additivePass);
28 return [radialBlurComposer, finalComposer, radialBlur, additivePass];
29}, []);

Additive shader pass

The additive shader pass is the one which combines the two rendered layers into one final texture which gets passed to the screen for displaying to the user.

1export default () => ({
2 uniforms: {
3 tDiffuse: { value: null },
4 tAdd: { value: null },
5 depthTexture: { value: null },
6 },
7 vertexShader: `
8 varying vec2 vUv;
9 void main() {
10 vUv = uv;
11 gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
12 }
13 `,
14 fragmentShader: `
15 varying vec2 vUv;
16 uniform sampler2D tDiffuse;
17 uniform sampler2D depthTexture;
18 uniform sampler2D tAdd;
19 void main()
20 {
21 gl_FragColor = texture2D(tDiffuse, vUv) + (texture2D(tAdd, vUv));
22 }
23 `,

Down sides to this method

The down sides to doing something like this is its complicated and requires expert knowledge to maintain, and if you get too close to the effect it currently has some artefacts.

More articles from theFrontDev

Fractured Cube in Blender with Bloom Postprocessing in React Three Fiber

In this tutorial, we will guide you through the process of creating a stunning 3D fractured cube in Blender, and then rendering it with React Three Fiber's Bloom postprocessing effect to add a beautiful glow and depth of field to the final output. We will cover the basics of modeling, texturing, and fracturing the cube in Blender, and then export the model to React Three Fiber for rendering. Additionally, we will walk you through the steps of setting up the Bloom postprocessing effect in React Three Fiber to enhance the visual quality of the rendered scene. By the end of this tutorial, you will have a beautiful 3D model that showcases your skills in both Blender and React Three Fiber, as well as a deeper understanding of how to use Bloom postprocessing to elevate the look of your renders.

April 2nd, 2023 · 3 min read

How to build environments in unity

A complete guide to getting started with unity to create amazing environments. Along with how to get assets from the unity asset store. Covering post processing, terrain tools and using materials inside of unity.

April 1st, 2023 · 6 min read
© 2021–2023 theFrontDev
Link to $https://twitter.com/TheFrontDevLink to $https://github.com/Richard-ThompsonLink to $https://www.linkedin.com/in/richard-thompson-248ba3111/