three.js辉光的两种实现方法,以及解决添加辉光后背景变成黑色的问题

1,128 阅读7分钟

一、问题描述:

想要实现在一个场景中含有辉光效果的物体也有普通物体,完成之后发现效果虽然实现了但是容器背景成了黑色,故在网上查找了一些大佬经验,总结了解决方法。

二、需理解的知识点:

  1. layers.set() 删除图层对象已有的所有对应关系,增加与参数指定的图层的对应关系。layers.enable()增加图层对象与参数指定图层的对应关系,不会删除原有的图层对于关系。
  2. layers.mask用 bit mask 表示当前图层对象与 0 - 31 中的哪些图层相对应。所属层所对应的比特位为 1,其他位位 0。

三、辉光效果实现:

1. 初始化渲染器,定义辉光渲染器 bloomComposer,全局渲染器 finalComposer

const initComposer = (el) => {
  // 去掉锯齿—1
  // 通过ShaderPass构造函数把FXAAShader着色器和uniforms构成的对象作为参数,创建一个锯齿通道FXAAShaderPass,然后把锯齿通道插入到composer中。
  const effectFXAA = new ShaderPass(FXAAShader);
  effectFXAA.uniforms['resolution'].value.set(0.6 / window.innerWidth, 0.6 / window.innerHeight); // 渲染区域Canvas画布宽高度 不一定是全屏,也可以是区域值
  effectFXAA.renderToScreen = true;
  // 去掉锯齿—1
  const renderScene = new RenderPass(scene, camera); // RenderPass这个通道会在当前场景(scene)和摄像机(camera)的基础上渲染出一个新场景,新建:
  // 添加光晕效果—2
  bloomPass = new UnrealBloomPass( // UnrealBloomPass通道可实现一个泛光效果。
    new THREE.Vector2(el.offsetWidth, el.offsetHeight),
    1.5,
    0,
    0.85,
  );
  bloomPass.threshold = params.bloomThreshold;
  bloomPass.strength = params.bloomStrength;
  bloomPass.radius = params.bloomRadius;

  // 着色器通道容器–放进容器里
  bloomComposer = new EffectComposer(renderer); // EffectComposer可以理解为着色器通道容器,着色器通道按照先后顺序添加进来并执行
  // bloomComposer.renderToScreen = true;
  bloomComposer.addPass(renderScene);
  bloomComposer.addPass(bloomPass); // 添加光晕效果
  bloomComposer.addPass(effectFXAA); // 去掉锯齿

  // 着色器通道容器–放进容器里
  const finalPass = new ShaderPass(
    new THREE.ShaderMaterial({
      uniforms: {
        baseTexture: { value: null },
        // finalPass读去了bloomComposer生成的texture(EffectComposer默认生成两张texture,一张用来读,一张用来写,renderTarget2.texture对应写入的那张)
        bloomTexture: { value: bloomComposer.renderTarget2.texture },
        tDiffuse: { value: null },
        glowColor: { value: null },
      },
      vertexShader: vertexshader,
      fragmentShader: fragmentshader,
      defines: {},
    }),
    'baseTexture',
  );
  finalPass.needsSwap = true;
  finalPass.material.uniforms.glowColor.value = new THREE.Color();
  finalComposer = new EffectComposer(renderer);
  finalComposer.addPass(renderScene);
  finalComposer.addPass(finalPass);
  finalComposer.addPass(effectFXAA);
};

2. 实现渲染函数 render()

方案一: 相机分层渲染

 const render2 = () => {
    camera.layers.set(BLOOM_SCENE);
    bloomComposer.render();

    camera.layers.set(ENTIRE_SCENE);
    finalComposer.render();
    requestAnimationFrame(render2);
  };

方案二: 相机不分层渲染

const render1 = () => {
  scene.traverse(darkenNonBloomed);
  bloomComposer.render();
  scene.traverse(restoreMaterial);
  finalComposer.render();
  requestAnimationFrame(render1);
};

const bloomIgnore = []; // 跟辉光光晕有关的变量
function darkenNonBloomed(obj) {
  if (obj instanceof THREE.Scene) {
    // 此处忽略Scene,否则场景背景会被影响
    materials.scene = obj.background;
    obj.background = null;
    // return;
  }
  if (
    obj instanceof THREE.Sprite ||
    bloomIgnore.includes(obj.type) ||
    (obj.isMesh && bloomLayer.test(obj.layers) === false) // 判断与辉光是否同层
  ) {
    materials[obj.uuid] = obj.material;
    // 将不需要辉光的材质设置为黑色
    obj.material = darkMaterial;
  }
}
function restoreMaterial(obj) {
  if (obj instanceof THREE.Scene) {
    obj.background = materials.scene;
    delete materials.scene;
    return;
  }
  if (materials[obj.uuid]) {
    obj.material = materials[obj.uuid];
    delete materials[obj.uuid];
  }
}

四、解决添加辉光效果之后背景变成黑色

寻找解决办法的时候有看到一种方法是修改three库中UnrealBloomPass.js的代码,我尝试之后并没有解决。

可以解决背景问题的是方法是修改着色器中fragmentshader为:

const fragmentshader = `uniform sampler2D baseTexture;
    uniform sampler2D bloomTexture;
    varying vec2 vUv;
    void main() {
    vec4 base_color = texture2D(baseTexture, vUv);
    vec4 bloom_color = texture2D(bloomTexture, vUv);

    float lum = 0.21 * bloom_color.r + 0.71 * bloom_color.g + 0.07 * bloom_color.b;
    gl_FragColor = vec4(base_color.rgb + bloom_color.rgb, max(base_color.a, lum));
    }`;

最后结果为:

参考:

github.com/mrdoob/thre…

zhuanlan.zhihu.com/p/652824252

完整代码:

<template>
  <div id="three" class="three" ref="three"></div>
</template>

<script setup>
  import * as THREE from 'three';
  import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; // 轨道控制
  import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'; // 效果合成器–EffectComposer依赖RenderPass.js、ShaderPass.js、CopyShader.js库
  import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'; // 虚幻效果通道
  import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'; // 渲染通道
  import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'; // 着色器通道
  import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader.js'; // 抗锯齿着色器
  import { onMounted } from 'vue';

  const fragmentshader = `uniform sampler2D baseTexture;
    uniform sampler2D bloomTexture;
    varying vec2 vUv;
    void main() {
    vec4 base_color = texture2D(baseTexture, vUv);
    vec4 bloom_color = texture2D(bloomTexture, vUv);

    float lum = 0.21 * bloom_color.r + 0.71 * bloom_color.g + 0.07 * bloom_color.b;
    gl_FragColor = vec4(base_color.rgb + bloom_color.rgb, max(base_color.a, lum));
    // gl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv ) );
    }`;

  const vertexshader = `varying vec2 vUv;
    void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }`;

  let scene, camera, renderer, bloomComposer, finalComposer;
  const ENTIRE_SCENE = 0, // 全部的,整个的场景
    BLOOM_SCENE = 1; // 光晕场景
  const bloomLayer = new THREE.Layers(); // 光晕层次-创建一个图层对象
  bloomLayer.set(BLOOM_SCENE); // 先把光晕层次设置光晕场景的层次1
  const darkMaterial = new THREE.MeshBasicMaterial({ color: 'black' }); // 跟辉光光晕有关的变量
  const materials = {}; // 跟辉光光晕有关的变量
  const params = {
    exposure: 1, // 暴露
    bloomStrength: 1.5, // 光晕强度
    bloomThreshold: 0, // 光晕阈值
    bloomRadius: 0, // 光晕半径
  };
  const init = (el) => {
    // 场景
    scene = new THREE.Scene();
    // 相机 el.offsetWidth, el.offsetHeight
    camera = new THREE.PerspectiveCamera(70, el.offsetWidth / el.offsetHeight, 1, 100000);
    camera.position.set(50, 50, 50);
    camera.position.y = 50;
    // 渲染器
    renderer = new THREE.WebGLRenderer({
      antialias: true, // 抗锯齿
      alpha: true,
    });

    renderer.setClearColor(0x000000, 0); // 设置背景颜色
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(el.offsetWidth, el.offsetHeight);
    renderer.toneMapping = THREE.ReinhardToneMapping;

    el.appendChild(renderer.domElement);
    // 环境光
    const light = new THREE.AmbientLight(0xffffff, 0.6);
    light.layers.enable(ENTIRE_SCENE);
    light.layers.enable(BLOOM_SCENE);
    scene.add(light);
    // 控制器
    const controls = new OrbitControls(camera, renderer.domElement);
    scene.add(new THREE.AxesHelper(100));
    window.onresize = () => {
      renderer.setSize(el.offsetWidth, el.offsetHeight);
      camera.aspect = el.offsetWidth / el.offsetHeight;
      camera.updateProjectionMatrix();
    };
  };
  // 添加演示物体
  function addCubes1() {
    const geometry = new THREE.BoxGeometry(20, 20, 10);
    // 正常方块
    const normalMtl = new THREE.MeshLambertMaterial({ color: 0x35c5ea });
    const normalBox = new THREE.Mesh(geometry, normalMtl);
    normalBox.position.z = -30;
    // normalBox.layers.enable(ENTIRE_SCENE); // 这行代码其实没有卵用,如果没有创建layer他会自动创建 设置图层为0,网格模型1默认图层是0,对应掩码是1
    console.log('查看网格模型默认图层掩码值normalBox', normalBox.layers.mask);
    scene.add(normalBox);
    // 通过Three.js渲染器渲染场景的时候,场景中的模型对象图层必须和相机对象的图层一样,模型对象才会被渲染出来,一般默认情况下,网格Mesh等模型对象和相机对象Camera默认的图层都是0,具体点说就是它们的图层属性.layers值Layers对象表示的图层都是0,0层对应的.mask属性值是1.
    // 发光方块
    const bloomMtl = new THREE.MeshLambertMaterial({ color: 0x35c5ea });
    const bloomBox = new THREE.Mesh(geometry, bloomMtl);
    bloomBox.position.z = 30;
    bloomBox.layers.enable(BLOOM_SCENE);
    console.log('查看网格模型默认图层掩码值bloomBox', bloomBox.layers.mask);
    scene.add(bloomBox);
  }
  function addCubes() {
    // 创建两个box, 将box进行layers进行分层是重要代码,camera默认渲染0层
    let texture = new THREE.TextureLoader().load('/src/assets/images/crate.gif');
    let texture1 = new THREE.TextureLoader().load('/src/assets/images/crate.gif');
    var geometry1 = new THREE.BoxGeometry(20, 20, 20);
    var material1 = new THREE.MeshBasicMaterial({
      map: texture,
    });
    var cube1 = new THREE.Mesh(geometry1, material1);
    // 重要代码,将当前创建的box分配到0层
    // cube1.layers.enable(ENTIRE_SCENE); // 这行代码其实没有卵用,如果没有创建layer他会自动创建 设置图层为0,网格模型1默认图层是0,对应掩码是1
    console.log('查看网格模型默认图层掩码值cube1', cube1.layers.mask);
    cube1.position.set(-30, 0, 0);
    scene.add(cube1);
    // 通过Three.js渲染器渲染场景的时候,场景中的模型对象图层必须和相机对象的图层一样,模型对象才会被渲染出来,一般默认情况下,网格Mesh等模型对象和相机对象Camera默认的图层都是0,具体点说就是它们的图层属性.layers值Layers对象表示的图层都是0,0层对应的.mask属性值是1.
    // 发光方块
    var geometry2 = new THREE.BoxGeometry(20, 20, 20);
    var material2 = new THREE.MeshBasicMaterial({
      map: texture1,
    });
    var cube2 = new THREE.Mesh(geometry2, material2);
    cube2.position.set(30, 0, 0);
    cube2.layers.enable(BLOOM_SCENE);
    console.log('查看网格模型默认图层掩码值cube2', cube2.layers.mask);
    scene.add(cube2);
  }

  function changeBloom() {
    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).onChange(function (value) {
      bloomPass.threshold = Number(value);
    });
    gui.add(params, 'bloomStrength', 0.0, 3.0).onChange(function (value) {
      bloomPass.strength = Number(value);
    });
    gui
      .add(params, 'bloomRadius', 0.0, 1.0)
      .step(0.01)
      .onChange(function (value) {
        bloomPass.radius = Number(value);
      });
  }

  let bloomPass;
  const initComposer = (el) => {
    // 去掉锯齿—1
    // 通过ShaderPass构造函数把FXAAShader着色器和uniforms构成的对象作为参数,创建一个锯齿通道FXAAShaderPass,然后把锯齿通道插入到composer中。
    const effectFXAA = new ShaderPass(FXAAShader);
    effectFXAA.uniforms['resolution'].value.set(0.6 / window.innerWidth, 0.6 / window.innerHeight); // 渲染区域Canvas画布宽高度 不一定是全屏,也可以是区域值
    effectFXAA.renderToScreen = true;
    // 去掉锯齿—1
    const renderScene = new RenderPass(scene, camera); // RenderPass这个通道会在当前场景(scene)和摄像机(camera)的基础上渲染出一个新场景,新建:
    // 添加光晕效果—2
    bloomPass = new UnrealBloomPass( // UnrealBloomPass通道可实现一个泛光效果。
      new THREE.Vector2(el.offsetWidth, el.offsetHeight),
      1.5,
      0,
      0.85,
    );
    bloomPass.threshold = params.bloomThreshold;
    bloomPass.strength = params.bloomStrength;
    bloomPass.radius = params.bloomRadius;

    // 着色器通道容器–放进容器里
    bloomComposer = new EffectComposer(renderer); // EffectComposer可以理解为着色器通道容器,着色器通道按照先后顺序添加进来并执行
    // bloomComposer.renderToScreen = true;
    bloomComposer.addPass(renderScene);
    bloomComposer.addPass(bloomPass); // 添加光晕效果
    bloomComposer.addPass(effectFXAA); // 去掉锯齿

    // 着色器通道容器–放进容器里
    const finalPass = new ShaderPass(
      new THREE.ShaderMaterial({
        uniforms: {
          baseTexture: { value: null },
          // finalPass读去了bloomComposer生成的texture(EffectComposer默认生成两张texture,一张用来读,一张用来写,renderTarget2.texture对应写入的那张)
          bloomTexture: { value: bloomComposer.renderTarget2.texture },
          tDiffuse: { value: null },
          glowColor: { value: null },
        },
        vertexShader: vertexshader,
        fragmentShader: fragmentshader,
        defines: {},
      }),
      'baseTexture',
    );
    finalPass.needsSwap = true;
    finalPass.material.uniforms.glowColor.value = new THREE.Color();
    finalComposer = new EffectComposer(renderer);
    finalComposer.addPass(renderScene);
    finalComposer.addPass(finalPass);
    finalComposer.addPass(effectFXAA);
  };
  // 不采用相机分层渲染
  const render1 = () => {
    scene.traverse(darkenNonBloomed);
    bloomComposer.render();
    scene.traverse(restoreMaterial);
    finalComposer.render();
    requestAnimationFrame(render1);
  };

  // 相机分层渲染
  const render2 = () => {
    camera.layers.set(BLOOM_SCENE);
    bloomComposer.render();

    camera.layers.set(ENTIRE_SCENE);
    finalComposer.render();
    requestAnimationFrame(render2);
  };

  const bloomIgnore = []; // 跟辉光光晕有关的变量
  function darkenNonBloomed(obj) {
    if (obj instanceof THREE.Scene) {
      // 此处忽略Scene,否则场景背景会被影响
      materials.scene = obj.background;
      obj.background = null;
      // return;
    }
    if (
      obj instanceof THREE.Sprite || // 此处忽略Sprite
      bloomIgnore.includes(obj.type) ||
      (obj.isMesh && bloomLayer.test(obj.layers) === false) // 判断与辉光是否同层
    ) {
      materials[obj.uuid] = obj.material;
      // 将不需要辉光的材质设置为黑色
      obj.material = darkMaterial;
    }
  }
  function restoreMaterial(obj) {
    if (obj instanceof THREE.Scene) {
      obj.background = materials.scene;
      delete materials.scene;
      return;
    }
    if (materials[obj.uuid]) {
      obj.material = materials[obj.uuid];
      delete materials[obj.uuid];
    }
  }

  const main = () => {
    const el = document.getElementById('three');
    init(el);
    initComposer(el);
    addCubes();
    addCubes1();
    changeBloom();
  };

  onMounted(() => {
    main();
    render2();
  });
</script>

<style>
  .three {
    position: relative;
    width: 800px;
    height: 600px;
    background: red;
  }
</style>