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:
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";78import * as THREE from "three";910const fragmentShader = `11 uniform sampler2D baseTexture;12 uniform sampler2D bloomTexture;1314 varying vec2 vUv;1516 void main() {1718 gl_FragColor =19 (texture2D(baseTexture, vUv) + texture2D(bloomTexture, vUv));20 }21`;2223const vertexShader = `24 varying vec2 vUv;2526 void main() {2728 vUv = uv;2930 gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);31 }32 `;3334const Effects = ({ children }) => {35 const { gl, scene, camera, size } = useThree();3637 const [bloomComposer, finalComposer] = useMemo(() => {38 const renderScene = new RenderPass(scene, camera);3940 const bloomPass = new UnrealBloomPass(41 new THREE.Vector2(window.innerWidth, window.innerHeight),42 1.5,43 0.4,44 0.8545 );4647 const bloomComposer = new EffectComposer(gl);48 bloomComposer.renderToScreen = false;49 bloomComposer.addPass(renderScene);50 bloomComposer.addPass(bloomPass);5152 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;6566 const finalComposer = new EffectComposer(gl);67 finalComposer.addPass(renderScene);68 finalComposer.addPass(finalPass);6970 return [bloomComposer, finalComposer];71 }, []);7273 useFrame((state) => {74 camera.layers.set(2);75 bloomComposer.render();76 camera.layers.set(1);77 finalComposer.render();78 }, 9);7980 return null;81};8283export 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);34 const bloomPass = new UnrealBloomPass(5 new THREE.Vector2(window.innerWidth, window.innerHeight),6 1.5,7 0.4,8 0.859 );1011 const bloomComposer = new EffectComposer(gl);12 bloomComposer.renderToScreen = false;13 bloomComposer.addPass(renderScene);14 bloomComposer.addPass(bloomPass);1516 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;2930 const finalComposer = new EffectComposer(gl);31 finalComposer.addPass(renderScene);32 finalComposer.addPass(finalPass);3334 return [bloomComposer, finalComposer];35}, []);
So we have a bloom composer which will give the effect below:
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.texture5 }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 Lights2<group ref={ref} name="object010" scale={[0.1, 0.1, 0.1]}>3 <mesh4 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>1213// example of none headlight mesh14<group name="object012" scale={[0.1, 0.1, 0.1]}>15 <mesh16 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"]} />23<ambientLight layers={2} intensity={20} color="white" />4<directionalLight5 layers={1}6 castShadow7 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 Shader2varying vec2 vUv;34void main() {56 vUv = uv;78 gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);9}
1// fragment Shader2uniform sampler2D baseTexture;3uniform sampler2D bloomTexture;45varying vec2 vUv;67void main() {89 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 below3vec4(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!