Search code examples
javascriptthree.jslightbloom

ThreeJS Selective Bloom For Specific Parts Of One Object Using Emission Map


In my project I want to display 3d objects which sometimes have small LED lights. The idea is, that these small lights need to be emitting some kind of bloom to make it look like they are glowing.

I've tried to apply the UnrealBloom however it is considered for the entire scene and not just for the parts that have the actual emission value (using an emission texture map).. the scene gets very blurry as well.

enter image description here

This is obviously not what I wanted. I only need the little red LED light bulp to glow not the entire object. However I have not yet found a way to tell the engine to only apply the bloom to where the emission map is pointing at.

I'm using a very simple code setup which is almost the same as the UnrealBloom Example:

How can I setup the emission texture correctly and make only the emissive parts of the object glow and prevent the unrealistically shiny surfaces and very blurry visuals?

UPDATE: Editable example of my setup is now available on JSFiddle!

<body style="margin:0px; overflow:hidden;">
<div id="bloom-solution">   
    <div id="body">
    
        <h2 id="info" style="
          color: rgb(255,255,255);
          position: fixed;
          top: 45%;
          left: 50%;
          transform: translate(-50%, -50%);
        ">loading scene, this might take a few seconds..</h2>
    
        <script type="x-shader/x-vertex" id="vertexshader">

            varying vec2 vUv;

            void main() {

                vUv = uv;

                gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

            }

        </script>

        <script type="x-shader/x-fragment" id="fragmentshader">

            uniform sampler2D baseTexture;
            uniform sampler2D bloomTexture;

            varying vec2 vUv;

            void main() {

                gl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv ) );

            }

        </script>
    
        <script type="module">
        
        import * as THREE from 'https://threejs.org/build/three.module.js'

        import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js'
        import { GLTFLoader } from 'https://threejs.org/examples/jsm/loaders/GLTFLoader.js'
        import { RGBELoader } from 'https://threejs.org/examples/jsm/loaders/RGBELoader.js'
        import { EffectComposer } from 'https://threejs.org/examples/jsm/postprocessing/EffectComposer.js';
        import { RenderPass } from 'https://threejs.org/examples/jsm/postprocessing/RenderPass.js';
        import { UnrealBloomPass } from 'https://threejs.org/examples/jsm/postprocessing/UnrealBloomPass.js';

        // RESOURCES ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        const COLOR_TEXTURE =       "https://cdn.jsdelivr.net/gh/MigerRepo/bloom-solution/RecordPlayer_Color.jpeg"
        const METALNESS_TEXTURE =   "https://cdn.jsdelivr.net/gh/MigerRepo/bloom-solution/RecordPlayer_Metalness.jpeg"
        const EMISSION_TEXTURE =    "https://cdn.jsdelivr.net/gh/MigerRepo/bloom-solution/RecordPlayer_Emission.jpeg"
        const ALPHA_TEXTURE =       "https://cdn.jsdelivr.net/gh/MigerRepo/bloom-solution/RecordPlayer_Alpha.jpeg"
        
        const TURNTABLE_MODEL =     "https://cdn.jsdelivr.net/gh/MigerRepo/bloom-solution/turntable_a111.glb"
        const HDRI_MAP =            "https://cdn.jsdelivr.net/gh/MigerRepo/bloom-solution/forest.hdr"

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        function $(e){return document.getElementById(e)}

        const container = document.createElement( 'div' )
        document.body.appendChild( container )

        const scene = new THREE.Scene()
        scene.background = new THREE.Color( new THREE.Color("rgb(250,244,227)") )
        scene.fog = new THREE.Fog( new THREE.Color("rgb(100, 100, 100)"), 10, 50 )

        const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 1000 )
        camera.position.set( 7, 3, 7 )

        const renderer = new THREE.WebGLRenderer( { antialias: true } )
        renderer.setPixelRatio( window.devicePixelRatio )
        renderer.setSize( window.innerWidth, window.innerHeight )
        renderer.toneMapping = THREE.ACESFilmicToneMapping
        renderer.outputEncoding = THREE.sRGBEncoding
        renderer.shadowMap.enabled = true
        renderer.shadowMap.type = THREE.PCFSoftShadowMap
        container.appendChild( renderer.domElement )
        
        const controls = new OrbitControls( camera, renderer.domElement )
        controls.minDistance = 1
        controls.enablePan = true
        controls.enableZoom = true;
        controls.enableDamping = true
        controls.dampingFactor = 0.1
        controls.rotateSpeed = 0.5
        
        const directionalLight = new THREE.DirectionalLight( new THREE.Color("rgb(255, 255, 255)"), 1 )
        directionalLight.castShadow = true
        directionalLight.shadow.camera.top = 4
        directionalLight.shadow.camera.bottom = - 4
        directionalLight.shadow.camera.left = - 4
        directionalLight.shadow.camera.right = 4
        directionalLight.shadow.camera.near = 0.1
        directionalLight.shadow.camera.far = 40
        directionalLight.shadow.camera.far = 40
        directionalLight.shadow.bias = - 0.002
        directionalLight.position.set( 0, 20, 20 )
        directionalLight.shadow.mapSize.width = 1024*4
        directionalLight.shadow.mapSize.height = 1024*4
        scene.add( directionalLight )

        scene.add( new THREE.CameraHelper( directionalLight.shadow.camera ) )

        var gltfLoader
        var model
        var mesh
        
        const pmremGenerator = new THREE.PMREMGenerator( renderer )
        pmremGenerator.compileEquirectangularShader()

        new RGBELoader().setDataType( THREE.UnsignedByteType ).load( HDRI_MAP, function ( texture ) {           
            const envMap = pmremGenerator.fromEquirectangular( texture ).texture
            scene.environment = envMap
            texture.dispose()
            pmremGenerator.dispose()
                
            gltfLoader = new GLTFLoader()
            gltfLoader.load( TURNTABLE_MODEL, function ( gltf ) {   
                model = gltf.scene
                model.position.y = 1
                model.traverse( function ( child ) {
                    if ( child.isMesh ) {
                        mesh = child
                        child.castShadow = true
                        child.receiveShadow = true
                        child.material.transparent = true           
                        child.material.envMapIntensity = 1
                        
                        $("info").style.display = "none";
                    }
                } );

                model.scale.set(15,15,15)

                scene.add( model )
                animate()
            } )
        });
        
        const animate = function () {

            requestAnimationFrame( animate )

            controls.update()

            renderer.render( scene, camera )

        };
        
        window.addEventListener( 'resize', function () {
            const width = window.innerWidth
            const height = window.innerHeight
            renderer.setSize( width, height )
            camera.aspect = width / height
            camera.updateProjectionMatrix()
        } )
        </script>
    </div>
</div>
</body>

Solution

  • That official example is overcomplicated, from my point of view. But the concept of selective bloom itself is simple enough:

    1. Make all non-bloomed objects totally black
    2. Render the scene with bloomComposer
    3. Restore materials/colors to previous
    4. Render the scene with finalComposer

    That's it. How to manage the darkening/blackening non-bloomed object and restore their materials, it's up to you.

    Here is an example (which seems complex, but actually it's not that much):

    body{
      overflow: hidden;
      margin: 0;
    }
    <script type="x-shader/x-vertex" id="vertexshader">
      varying vec2 vUv;
      void main() {
        vUv = uv;
        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
      }
    </script>
    <script type="x-shader/x-fragment" id="fragmentshader">
      uniform sampler2D baseTexture;
      uniform sampler2D bloomTexture;
      varying vec2 vUv;
      void main() {
        gl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv ) );
      }
    </script>
    <script type="module">
    import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
    
    import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/OrbitControls.js';
    
    import { EffectComposer } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/EffectComposer.js';
    import { RenderPass } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/RenderPass.js';
    import { ShaderPass } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/ShaderPass.js';
    import { UnrealBloomPass } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/UnrealBloomPass.js';
    
    let scene = new THREE.Scene();
    let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 100);
    camera.position.set(0, 3, 5);
    let renderer = new THREE.WebGLRenderer();
    renderer.setSize(innerWidth, innerHeight);
    //renderer.setClearColor(0x404040);
    document.body.appendChild(renderer.domElement);
    
    let controls = new OrbitControls(camera, renderer.domElement);
    
    let light = new THREE.DirectionalLight(0xffffff, 0.5);
    light.position.setScalar(1);
    scene.add(light, new THREE.AmbientLight(0xffffff, 0.5));
    
    let uniforms = {
      globalBloom: {value: 1}
    }
    
    // texture
    new THREE.TextureLoader().load("https://threejs.org/examples/textures/hardwood2_diffuse.jpg", tex => {
      //console.log(tex);
      
      let img = tex.image;
      
      let c = document.createElement("canvas");
      let min = Math.min(img.width, img.height);
      c.width = c.height = min;
      let ctx = c.getContext("2d");
      ctx.drawImage(img, 0, 0);
      
      let c2 = document.createElement("canvas");
      c2.width = c2.height = min;
      let ctx2 = c2.getContext("2d");
      ctx2.clearRect(0, 0, min, min); 
      
      ["#f00", "#0f0", "#ff0", "#f0f", "#0ff"].forEach( (col, i, a) => {
          let id = i - ((a.length - 1) / 2);
          let dist = id * 150;
          //console.log(dist, col, i, c.width, c.height);
          ctx.beginPath();
          ctx.arc(min * 0.5 + dist, min * 0.5, 25, 0, 2 * Math.PI);
          ctx.fillStyle = col;
          ctx.fill();
        }
      );
      
      let cTex = new THREE.CanvasTexture(c);
      let c2Tex = new THREE.CanvasTexture(c2);
      
      setInterval(() => {
        ctx2.clearRect(0, 0, min, min);
        let id = THREE.MathUtils.randInt(0, 4) - 2;
        let dist = id * 150;
        ctx2.beginPath();
        ctx2.arc(min * 0.5 + dist, min * 0.5, 25, 0, 2 * Math.PI);
        ctx2.fillStyle = "#fff";
        ctx2.fill();
        c2Tex.needsUpdate = true;
      }, 125);
      
      let g = new THREE.PlaneGeometry(5, 5);
      g.rotateX(Math.PI * -0.5);
      let m = new THREE.MeshStandardMaterial(
        {
          roughness: 0.6,
          metalness: 0.5,
          map: cTex,
          emissiveMap: c2Tex,
          onBeforeCompile: shader => {
            shader.uniforms.globalBloom = uniforms.globalBloom;
            shader.fragmentShader = `
                uniform float globalBloom;
              ${shader.fragmentShader}
            `.replace(
                `#include <dithering_fragment>`,
              `#include <dithering_fragment>
                vec3 col = texture2D( map, vUv).rgb;
                float em = texture2D( emissiveMap, vUv ).g;
                col *= em;
                gl_FragColor.rgb = mix(gl_FragColor.rgb, col, globalBloom);
                
              `
            );
            console.log(shader.fragmentShader);
          }
        }
      );
      let o = new THREE.Mesh(g, m);
      scene.add(o);
      
    })
    
    window.onresize = function () {
    
      const width = window.innerWidth;
      const height = window.innerHeight;
    
      camera.aspect = width / height;
      camera.updateProjectionMatrix();
    
      renderer.setSize( width, height );
    
      bloomComposer.setSize( width, height );
      finalComposer.setSize( width, height );
    
    };
    
    // bloom
    const renderScene = new RenderPass( scene, camera );
    
    const bloomPass = new UnrealBloomPass( new THREE.Vector2( window.innerWidth, window.innerHeight ), 1.5, 0, 0.1 );
    
    const bloomComposer = new EffectComposer( renderer );
    bloomComposer.renderToScreen = false;
    bloomComposer.addPass( renderScene );
    bloomComposer.addPass( bloomPass );
    
    const finalPass = new ShaderPass(
      new THREE.ShaderMaterial( {
        uniforms: {
          baseTexture: { value: null },
          bloomTexture: { value: bloomComposer.renderTarget2.texture }
        },
        vertexShader: document.getElementById( 'vertexshader' ).textContent,
        fragmentShader: document.getElementById( 'fragmentshader' ).textContent,
        defines: {}
      } ), "baseTexture"
    );
    finalPass.needsSwap = true;
    
    const finalComposer = new EffectComposer( renderer );
    finalComposer.addPass( renderScene );
    finalComposer.addPass( finalPass );
    
    renderer.setAnimationLoop( _ => {
        
      renderer.setClearColor(0x000000);
      uniforms.globalBloom.value = 1;
      
      bloomComposer.render();
      
      renderer.setClearColor(0x202020);
      uniforms.globalBloom.value = 0;
      
        finalComposer.render();
      //renderer.render(scene, camera);
    })
    
    </script>