Search code examples
javascriptthree.jsglsl

GLSL In ThreeJS Mix FragColor With UnrealBloom To Get Selective Glow


I want to implement selective bloom for an imported GLTF model in ThreeJS using an Emission map.

To achieve this I am supposed to first make the objects that should not have bloom completely black and using the UnrealBloomPass and the ShaderPass I'm going to mix the bloomed and non-bloomed effect passes together somehow.

I need to use GLSL code, which I'm only barely familiar with. Here is my basic setup:

View Example In JSFiddle

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Render View</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
    </head>

    <body id="body" style="overflow: hidden">
        <h1 id="info">loading scene, this might take a few seconds..</h1>
        
        <!-- Warning: xhr progress seems to not work over cnd.jsdelivr -->
        <!-- <div id="loading" id="myProgress"><div id="myBar"></div></div> -->
        
        <input class="tool" id="backgroundColor" type="color" value="#2e2e2e">

        <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 Stats from 'https://threejs.org/examples/jsm/libs/stats.module.js'
        import { GUI } from 'https://threejs.org/examples/jsm/libs/dat.gui.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 { ShaderPass } from 'https://threejs.org/examples/jsm/postprocessing/ShaderPass.js';
        import { RenderPass } from 'https://threejs.org/examples/jsm/postprocessing/RenderPass.js';
        import { UnrealBloomPass } from 'https://threejs.org/examples/jsm/postprocessing/UnrealBloomPass.js';

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

        function createContainer() {
            var ctn = document.createElement( 'div' )
            document.body.appendChild( ctn )
            return ctn
        }
        
        function createScene() {
            var scn = new THREE.Scene()
            scn.fog = new THREE.Fog( new THREE.Color("rgb(100, 100, 100)"), 40, 150 )
            return scn
        }

        function createCamera() {
            var cam = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 1000 )
            cam.position.set( 20, 12, 20 )
            return cam
        }

        function createRenderer() {
            var rnd = new THREE.WebGLRenderer( { antialias: true } )
            rnd.setPixelRatio( window.devicePixelRatio )
            rnd.setSize( window.innerWidth, window.innerHeight )
            rnd.toneMapping = THREE.ReinhardToneMapping
            rnd.outputEncoding = THREE.sRGBEncoding
            rnd.shadowMap.enabled = true
            rnd.shadowMap.type = THREE.PCFSoftShadowMap
            container.appendChild( rnd.domElement )
            return rnd
        }

        function createControls() {
            var ctr = new OrbitControls( camera, renderer.domElement )
            ctr.minDistance = 1
            ctr.maxDistance = 50
            ctr.enablePan = true
            ctr.enableZoom = true
            ctr.enableDamping = true
            ctr.dampingFactor = 0.1
            ctr.rotateSpeed = 0.5
            return ctr
        }
        
        function createDirectionalLight() {
            var drt = new THREE.DirectionalLight( new THREE.Color("rgb(255, 255, 255)"), 1 )
            drt.castShadow = true
            drt.shadow.camera.top = 64
            drt.shadow.camera.top = 64
            drt.shadow.camera.bottom = - 64
            drt.shadow.camera.left = - 64
            drt.shadow.camera.right = 64
            drt.shadow.camera.near = 0.2
            drt.shadow.camera.far = 80
            drt.shadow.camera.far = 80
            drt.shadow.bias = - 0.002
            drt.position.set( 0, 20, 20 )
            drt.shadow.mapSize.width = 1024*8
            drt.shadow.mapSize.height = 1024*8
            // scene.add( new THREE.CameraHelper( drt.shadow.camera ) )
            scene.add( drt )
            return drt
        }

        function createSceneBounds() {
            var cube = new THREE.Mesh( new THREE.BoxGeometry( 20, 20, 20 ) )
            cube.position.y = 0
            cube.visible = false
            scene.add(cube)
            return new THREE.Box3().setFromObject( cube );
        }
        
        function createBloomPass() {
            var blp = new UnrealBloomPass( new THREE.Vector2( window.innerWidth, window.innerHeight ), 1.5, 0.4, 0.85 )
            blp.threshold = params.bloomThreshold
            blp.strength = params.bloomStrength
            blp.radius = params.bloomRadius
            return blp
        }
                
        function createFinalPass() {
            var shp = 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"
            );
            shp.needsSwap = true
            return shp
        }
        
        function createEnvironment( hdr, onLoad ) {
            new RGBELoader().setDataType( THREE.UnsignedByteType ).load( hdr, function ( texture ) {        
                const pmremGenerator = new THREE.PMREMGenerator( renderer )
                pmremGenerator.compileEquirectangularShader()
                
                const envMap = pmremGenerator.fromEquirectangular( texture ).texture
                scene.environment = envMap
                texture.dispose()
                pmremGenerator.dispose()
                    
                onLoad()
            } );
        }
        
        function loadGLTF( file, onLoad ) {     
            new GLTFLoader().load( file , onLoad, function( xhr ) {
                
                // Warning: xhr progress seems to not work over cnd.jsdelivr
                // if ( xhr.lengthComputable ) {
                    // var percentComplete = xhr.loaded / xhr.total * 100
                    // var elem = document.getElementById("myBar");
                    // elem.style.width = Math.round(percentComplete, 2) + "%";
                    // console.log( "Loading Model - " + Math.round(percentComplete, 2) + "%" )
                    
                // }
                
            })
        }
        
        const params = {
            exposure: 1,
            bloomStrength: 5,
            bloomThreshold: 0,
            bloomRadius: 0,
            scene: "Scene with Glow"
        };

        const container = createContainer() 
        
        const scene = createScene()
        
        const camera = createCamera()   
        
        const renderer = createRenderer()
        
        const controls = createControls()
        
        const directionalLight = createDirectionalLight()
        
        const sceneBounds = createSceneBounds()
                
        const renderScene = new RenderPass( scene, camera ) 
        
        const bloomPass = createBloomPass()
        const bloomComposer = new EffectComposer( renderer )
        bloomComposer.addPass( renderScene )
        bloomComposer.addPass( bloomPass )
        
        const finalPass = createFinalPass()
        const finalComposer = new EffectComposer( renderer )
        finalComposer.addPass( renderScene )
        finalComposer.addPass( finalPass )  

        var model = null
        var importedMaterial = null
        var emissiveMaterial = null

        var mesh = null         
        var meshBounds = null

        createEnvironment( "https://cdn.jsdelivr.net/gh/MigerRepo/bloom-solution/forest.hdr", function() {
            
            loadGLTF( "https://cdn.jsdelivr.net/gh/MigerRepo/bloom-solution/turntable_121.glb", function ( gltf ) {
                model = gltf.scene
                model.traverse( function ( child ) {
                    if ( child.isMesh ) {
                        mesh = child
                        
                        // enable shadows
                        mesh.castShadow = true
                        mesh.receiveShadow = true
                        
                        // set original material
                        importedMaterial = mesh.material
                        importedMaterial.envMapIntensity = 1
                        
                        // assign temporary black material
                        mesh.material = new THREE.MeshBasicMaterial({color: 0x000000})
        
                        // assign bloom only material
                        new THREE.TextureLoader()
            .load("https://cdn.jsdelivr.net/gh/MigerRepo/bloom-solution/RecordPlayer_Emission.jpeg",
                function( texture ) {
                  texture.flipY = false
                  texture.encoding = THREE.sRGBEncoding
                  emissiveMaterial = new THREE.MeshBasicMaterial({map: texture});
                  mesh.material = emissiveMaterial
                })                                          
                    }
                });
                        
                fitObjectToSceneBounds()
                scene.add( model )
                
                $("info").style.display = "none"
                
                // Warning: xhr progress seems to not work over cnd.jsdelivr
                // $("loading").style.display = "none"
                
                animate()
            })
            
        })
                
        const animate = function () {

            requestAnimationFrame( animate )

            // set background color
            scene.background = new THREE.Color( new THREE.Color( $("backgroundColor").value ) )
            $('body').attributes['style'].textContent='background-color:'+ $("backgroundColor").value

            controls.update()

            bloomComposer.render()
            
            // finalComposer.render()

        };
        
        window.addEventListener( 'resize', function () {
            const width = window.innerWidth
            const height = window.innerHeight
            
            renderer.setSize( width, height )
            bloomComposer.setSize( width, height );
            finalComposer.setSize( width, height );
            
            camera.aspect = width / height
            camera.updateProjectionMatrix()
        })
            
        function fitCameraToSelection( camera, controls, selection, fitOffset = 1 ) {     
            const box = new THREE.Box3()
            try {
                for( const object of selection ) {
                    box.expandByObject( object )
                }
            } catch( e ) { box.expandByObject( selection ) }      
            const size = box.getSize( new THREE.Vector3() )
            const center = box.getCenter( new THREE.Vector3() )   
            const maxSize = Math.max( size.x, size.y, size.z )
            const fitHeightDistance = maxSize / ( 1.7 * Math.atan( Math.PI * camera.fov / 360 ) )
            const fitWidthDistance = fitHeightDistance / camera.aspect
            const distance = fitOffset * Math.max( fitHeightDistance, fitWidthDistance )  
            const direction = controls.target.clone().sub( camera.position ).normalize().multiplyScalar( distance )
            controls.maxDistance = distance * 10
            controls.target.copy( center ) 
            camera.near = distance / 100
            camera.far = distance * 100
            camera.updateProjectionMatrix()
            camera.position.copy( controls.target ).sub(direction);  
            controls.update()  
        }
        
        function fitObjectToSceneBounds() {
            meshBounds = new THREE.Box3().setFromObject( model )
            let lengthSceneBounds = {
              x: Math.abs(sceneBounds.max.x - sceneBounds.min.x),
              y: Math.abs(sceneBounds.max.y - sceneBounds.min.y),
              z: Math.abs(sceneBounds.max.z - sceneBounds.min.z),
            };
            let lengthMeshBounds = {
              x: Math.abs(meshBounds.max.x - meshBounds.min.x),
              y: Math.abs(meshBounds.max.y - meshBounds.min.y),
              z: Math.abs(meshBounds.max.z - meshBounds.min.z),
            };
            let lengthRatios = [
              (lengthSceneBounds.x / lengthMeshBounds.x),
              (lengthSceneBounds.y / lengthMeshBounds.y),
              (lengthSceneBounds.z / lengthMeshBounds.z),
            ];
            let minRatio = Math.min(...lengthRatios)
            let padding = 0
            minRatio -= padding
            model.scale.set(minRatio, minRatio, minRatio)
        }
        
        const gui = new GUI();

        gui.add( params, 'exposure', 0.1, 2 ).onChange( function ( value ) {

            renderer.toneMappingExposure = Math.pow( value, 4.0 );

        } );

        gui.add( params, 'bloomThreshold', 0.0, 1.0 ).step( 0.001 ).onChange( function ( value ) {

            bloomPass.threshold = Number( value );

        } );

        gui.add( params, 'bloomStrength', 0.0, 20.0 ).step( 0.01 ).onChange( function ( value ) {

            bloomPass.strength = Number( value );

        } );

        gui.add( params, 'bloomRadius', 0.0, 5.0 ).step( 0.01 ).onChange( function ( value ) {

            bloomPass.radius = Number( value );

        } );
        
        </script>
    </body>
</html>

When you look at the result in the JSFiddle you can see that the glow is nicely visible and the rest of the object is black.

Now I need to know how I can use GLSL to combine the fragment shader with the UnrealBloomPass and like mix both results together. However I do not really know how to do that since GLSL is not something I have a lot of experience with it and I don't know how to use it in combination with ThreeJS.

How can I get the selective bloom working?


Solution

  • The order for selective bloom is still the same:

    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

    Patch model's material, having a common uniform, that indicates which render will be used:

    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">
    console.clear();
    import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
    import {GLTFLoader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/GLTFLoader.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 model;
    
    let scene = new THREE.Scene();
    let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100);
    camera.position.set(7.7, 2.5, 7.2);
    let renderer = new THREE.WebGLRenderer();
    renderer.setSize(innerWidth, innerHeight);
    //renderer.setClearColor(0x404040);
    document.body.appendChild(renderer.domElement);
    
    let controls = new OrbitControls(camera, renderer.domElement);
    controls.addEventListener("change", e => {console.log(camera.position)})
    
    let light = new THREE.DirectionalLight(0xffffff, 1.5);
    light.position.setScalar(1);
    scene.add(light, new THREE.AmbientLight(0xffffff, 0.5));
    
    let uniforms = {
      globalBloom: {value: 1}
    }
    
    let loader = new GLTFLoader();
    loader.load( "https://cdn.jsdelivr.net/gh/MigerRepo/bloom-solution/turntable_121.glb", function ( gltf ) {
        model = gltf.scene;
      let emssvTex = new THREE.TextureLoader().load("https://cdn.jsdelivr.net/gh/MigerRepo/bloom-solution/RecordPlayer_Emission.jpeg", function( texture ) {
        texture.flipY = false
        texture.encoding = THREE.sRGBEncoding
      })    
      model.traverse( function ( child ) {
        if ( child.isMesh ) {
            child.material.emissiveMap = emssvTex;
          child.material.onBeforeCompile = shader => {
            shader.uniforms.globalBloom = uniforms.globalBloom;
            shader.fragmentShader = `
                uniform float globalBloom;
              ${shader.fragmentShader}
            `.replace(
                `#include <dithering_fragment>`,
              `#include <dithering_fragment>
                if (globalBloom > 0.5){
                    gl_FragColor = texture2D( emissiveMap, vUv );
                }
              `
            );
            console.log(shader.fragmentShader);
          }
        }
      });
      model.scale.setScalar(40);
      scene.add(model);
    });
    
    // bloom
    const renderScene = new RenderPass( scene, camera );
    
    const bloomPass = new UnrealBloomPass( new THREE.Vector2( window.innerWidth, window.innerHeight ), 2, 0, 0 );
    
    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 );
    
    window.onresize = function () {
    
      camera.aspect = innerWidth / innerHeight;
      camera.updateProjectionMatrix();
    
      renderer.setSize( innerWidth, innerHeight );
    
      bloomComposer.setSize( innerWidth, innerHeight );
      finalComposer.setSize( innerWidth, innerHeight );
    
    };
    
    renderer.setAnimationLoop( _ => {
        
      renderer.setClearColor(0x000000);
      uniforms.globalBloom.value = 1;
      
      bloomComposer.render();
      
      renderer.setClearColor(0x404040);
      uniforms.globalBloom.value = 0;
      
        finalComposer.render();
      //renderer.render(scene, camera);
    })
    
    </script>