ThreeJS 中的 GLSL 将 FragColor 与 UnrealBloom 混合以获得选择性发光

GLSL In ThreeJS Mix FragColor With UnrealBloom To Get Selective Glow

我想使用 Emission 贴图在 ThreeJS 中为导入的 GLTF 模型实现选择性绽放。

为了实现这一点,我应该首先使不应该绽放的对象完全变黑,然后使用 UnrealBloomPass 和 ShaderPass 我将以某种方式将绽放和非绽放效果混合在一起。

我需要使用我不太熟悉的 GLSL 代码。这是我的基本设置:

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>

当您查看 JSFiddle 中的结果时,您会发现光晕清晰可见,而对象的其余部分是黑色的。

现在我需要知道如何使用 GLSL 将片段着色器与 UnrealBloomPass 结合起来,以及如何将这两种结果混合在一起。但是我真的不知道该怎么做,因为我对 GLSL 没有太多经验,而且我不知道如何将它与 ThreeJS 结合使用。

我怎样才能使选择性绽放起作用?

选择性绽放的顺序还是一样:

  1. 使所有非泛光对象完全变黑
  2. 使用 bloomComposer 渲染场景
  3. 恢复materials/colors到之前的
  4. 使用 finalComposer 渲染场景

补丁模型的 material,具有共同的制服,表明将使用哪个渲染器:

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/three@0.127.0/build/three.module.js';
import {GLTFLoader} from 'https://cdn.jsdelivr.net/npm/three@0.127.0/examples/jsm/loaders/GLTFLoader.js';

import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.127.0/examples/jsm/controls/OrbitControls.js';

import { EffectComposer } from 'https://cdn.jsdelivr.net/npm/three@0.127.0/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'https://cdn.jsdelivr.net/npm/three@0.127.0/examples/jsm/postprocessing/RenderPass.js';
import { ShaderPass } from 'https://cdn.jsdelivr.net/npm/three@0.127.0/examples/jsm/postprocessing/ShaderPass.js';
import { UnrealBloomPass } from 'https://cdn.jsdelivr.net/npm/three@0.127.0/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>