FDNavigate back to the homepage

Bloom and Chaining Effect Composers, Whilst Using Three's Layers

Rick
December 30th, 2021 · 2 min read

This is something I did some time ago and thought it deserved a write up. At the time I did this research I was doing a personal project.. a car configurator.

A stunning effect mimicing glowing objects is called bloom.

All the examples I researched online and on codesandboxes didnt seem to quite work or looked like hacks.

Hopefully this way of doing selective bloom will help you to achieve this effect in R3F.

Here is the general approach:

Diagram of how to do bloom, layers and chaining effect composers

A small note is that it just wouldnt work declaratively. I had to code some effect composers the old three way in a useMemo. Believe me i spent a good day trying to get this to work declaratively.

Here is the final codesandbox:

The principle is seperating bloom’ed meshes into one layer and then the rest of the model into the other layer. Much like you could do in image editing software or blender

Performing bloom on selected layer and once complete, render the bloom composer and then render the composer with the rest of the objects in and merge.

Composers

Here is the code of the effects component:

1import React, { useMemo } from "react";
2import { useThree, useFrame } from "@react-three/fiber";
3import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer";
4import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass";
5import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass";
6import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass";
7
8import * as THREE from "three";
9
10const fragmentShader = `
11 uniform sampler2D baseTexture;
12 uniform sampler2D bloomTexture;
13
14 varying vec2 vUv;
15
16 void main() {
17
18 gl_FragColor =
19 (texture2D(baseTexture, vUv) + texture2D(bloomTexture, vUv));
20 }
21`;
22
23const vertexShader = `
24 varying vec2 vUv;
25
26 void main() {
27
28 vUv = uv;
29
30 gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
31 }
32 `;
33
34const Effects = ({ children }) => {
35 const { gl, scene, camera, size } = useThree();
36
37 const [bloomComposer, finalComposer] = useMemo(() => {
38 const renderScene = new RenderPass(scene, camera);
39
40 const bloomPass = new UnrealBloomPass(
41 new THREE.Vector2(window.innerWidth, window.innerHeight),
42 1.5,
43 0.4,
44 0.85
45 );
46
47 const bloomComposer = new EffectComposer(gl);
48 bloomComposer.renderToScreen = false;
49 bloomComposer.addPass(renderScene);
50 bloomComposer.addPass(bloomPass);
51
52 const finalPass = new ShaderPass(
53 new THREE.ShaderMaterial({
54 uniforms: {
55 baseTexture: { value: null },
56 bloomTexture: { value: bloomComposer.renderTarget2.texture },
57 },
58 vertexShader: vertexShader,
59 fragmentShader: fragmentShader,
60 defines: {},
61 }),
62 "baseTexture"
63 );
64 finalPass.needsSwap = true;
65
66 const finalComposer = new EffectComposer(gl);
67 finalComposer.addPass(renderScene);
68 finalComposer.addPass(finalPass);
69
70 return [bloomComposer, finalComposer];
71 }, []);
72
73 useFrame((state) => {
74 camera.layers.set(2);
75 bloomComposer.render();
76 camera.layers.set(1);
77 finalComposer.render();
78 }, 9);
79
80 return null;
81};
82
83export default Effects;

It might seam an anti pattern doing the below in a none declarative way but in my eyes this is completely fine especially when dealing with things like this, when gl render order is very important.. and the react rendering timings could get in the way:

1const [bloomComposer, finalComposer] = useMemo(() => {
2 const renderScene = new RenderPass(scene, camera);
3
4 const bloomPass = new UnrealBloomPass(
5 new THREE.Vector2(window.innerWidth, window.innerHeight),
6 1.5,
7 0.4,
8 0.85
9 );
10
11 const bloomComposer = new EffectComposer(gl);
12 bloomComposer.renderToScreen = false;
13 bloomComposer.addPass(renderScene);
14 bloomComposer.addPass(bloomPass);
15
16 const finalPass = new ShaderPass(
17 new THREE.ShaderMaterial({
18 uniforms: {
19 baseTexture: { value: null },
20 bloomTexture: { value: bloomComposer.renderTarget2.texture },
21 },
22 vertexShader: vertexShader,
23 fragmentShader: fragmentShader,
24 defines: {},
25 }),
26 "baseTexture"
27 );
28 finalPass.needsSwap = true;
29
30 const finalComposer = new EffectComposer(gl);
31 finalComposer.addPass(renderScene);
32 finalComposer.addPass(finalPass);
33
34 return [bloomComposer, finalComposer];
35}, []);

So we have a bloom composer which will give the effect below:

Bloom effect example

And then we have a final composer which will render the layer which wont have bloom applied.

The most important part here is passing the renderTarget from the built in bloom effect to the custom final composer:

1uniforms: {
2 baseTexture: { value: null },
3 bloomTexture: {
4 value: bloomComposer.renderTarget2.texture
5 }
6},

Understanding how to access this renderTarget, ie the property name of it took some googling haha

What does layering look like?

Take the below for example:

1// Head Lights
2<group ref={ref} name="object010" scale={[0.1, 0.1, 0.1]}>
3 <mesh
4 name="object010_glass_0"
5 layers={2}
6 geometry={nodes.object010_glass_0.geometry}
7 // material={materials.glass}
8 >
9 <meshStandardMaterial attach="material" color="white" />
10 </mesh>
11</group>
12
13// example of none headlight mesh
14<group name="object012" scale={[0.1, 0.1, 0.1]}>
15 <mesh
16 name="object012_Material001_0"
17 layers={1}
18 geometry={nodes.object012_Material001_0.geometry}
19 material={materials["Material.001"]}
20 />
21</group>

It might seem this simple however there are more considerations 🙂

You need lights in both layers otherwise this wont work. This is the light setup I had:

1<color attach="background" args={["black"]} />
2
3<ambientLight layers={2} intensity={20} color="white" />
4<directionalLight
5 layers={1}
6 castShadow
7 position={[2.5, 8, 5]}
8 intensity={5.5}
9 shadow-mapSize-width={1024}
10 shadow-mapSize-height={1024}
11 shadow-camera-far={50}
12 shadow-camera-left={-10}
13 shadow-camera-right={10}
14 shadow-camera-top={10}
15 shadow-camera-bottom={-10}
16 color="white"
17/>

Notice how we user the layers prop as we did on parts of the mesh, pretty cool eh 😎.

So how exactly do we render the different composers, such that the bloom effect is done first and passed to the final cumulative composer?

1useFrame((state) => {
2 camera.layers.set(2);
3 bloomComposer.render();
4 camera.layers.set(1);
5 finalComposer.render();
6}, 1);

First off we access the camera through useThree and set the layers to be 2 (the layer with just the headlights in). Then we redner the bloom composer. Set the layers to be 1 (the rest of the scene minus the headlights) and finally render the finalComposer.

This sequence allows us to perform bloom, pass the texture to the final composer and render the final composer.

Within this composer we mix the two textures (bloom and none bloom).

Final Composer

What does the shader code look like in the final composer?

1// Vertex Shader
2varying vec2 vUv;
3
4void main() {
5
6 vUv = uv;
7
8 gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
9}
1// fragment Shader
2uniform sampler2D baseTexture;
3uniform sampler2D bloomTexture;
4
5varying vec2 vUv;
6
7void main() {
8
9 gl_FragColor =
10 texture2D(baseTexture, vUv) + texture2D(bloomTexture, vUv);
11}

The vertex shader is pretty bog standard. The fragment shader is where the magic happens.

We sample the baseTexture and the bloom texture and simply add these two. Adding two colours increases intensity of the colour.

1vec4(0.1, 0.1, 0.1, 1.0);
2// Above is less bright than below
3vec4(1.0, 1.0, 1.0, 1.0);

Then we have a final texture which is rendered which combines the bloom and the normal rendered remaining scene.

Final Thoughts

So 2021 has been a blast, I have tried to provide interesting content that people studying R3F / Three would want. Or content I would have wanted when I am learning more intermediate or advanced effects.

Lots of love being sent to everyone as 2021 draws to a close 😘.

Also have a play around with the codesandbox and let me know what you think!

I have lots of ideas for 2022 and am confident there will be lots more articles about R3F, blender and 3D in general.

See you in 2022!

More articles from theFrontDev

Volumetric Rendering in R3F of 3D Noise in a 2D Texture

A proof of concept to render 3D noise sliced into rows of a 2D texture. For effects like fog, clouds sand storms etc. Have a play with the codesandbox and change the settings!

December 28th, 2021 · 4 min read

Generating a Flow Map from Blender to React Three Fiber

Generating a Flow map in blender and using this to mimic flowing water in a react three fiber setting.

December 5th, 2021 · 2 min read
© 2021–2024 theFrontDev
Link to $https://twitter.com/TheFrontDevLink to $https://github.com/Richard-ThompsonLink to $https://www.linkedin.com/in/richard-thompson-248ba3111/