Three.js中使用bloom特效 + 常见的Pass总结

shader小白的学习之路。

最近在研究three.js中有关后处理的部分。后处理是指在渲染场景后对图像进行额外处理,以实现各种视觉效果,如模糊、景深、颜色校正等。

这里做了一些总结,包括UnrealBloomPass的使用、描边、闪屏等Pass的基础介绍。

1.UnrealBloomPass辉光特效

在后处理中,有两个Pass可以做到辉光效果,分别为BloomPass和UnrealBloomPass,这里先简单介绍一下二者(主要也是为自己之前的一些疑问做个总结)。

UnrealBloomPass 是一个更高级的泛光效果实现,通常用于模拟类似 Unreal Engine 中的高质量泛光效果。它通过以下方式实现泛光效果:

  • 高斯模糊:对场景中的高亮度区域进行高斯模糊处理。

  • 屏幕混合:将模糊后的高亮度区域与原始场景混合,从而实现逼真的泛光效果。

  • 参数控制:可以通过 strength(强度)、threshold(阈值)和 radius(模糊半径)等参数来精细调整效果。

UnrealBloomPass 的优点是效果逼真,适合用于需要高质量泛光的场景,但它可能会对性能产生一定影响。

BloomPass

BloomPass 是一个更基础的泛光实现,通常用于简单的场景。它通过以下方式实现泛光效果:

  • 亮度提取:提取场景中的高亮度区域。

  • 模糊处理:对这些高亮度区域进行模糊处理。

  • 混合输出:将模糊后的区域与原始场景混合。

BloomPass 的实现相对简单,适合用于对性能要求较高的场景,但效果可能不如 UnrealBloomPass 逼真。

二者如何选择

  • 效果质量UnrealBloomPass 提供更高质量和更逼真的泛光效果,而 BloomPass 更适合简单场景。

  • 性能UnrealBloomPass 由于其复杂的实现(如高斯模糊和屏幕混合),可能会对性能产生较大影响,而 BloomPass 更轻量。

  • 参数控制UnrealBloomPass 提供更丰富的参数(如 strengththresholdradius),允许更精细的调整。

相比之下,UnrealBloomPass的效果更好一些,这里使用UnrealBloomPass一个机械手臂的辉光特效。

机械手臂辉光效果

效果图:

看起来也算是有点科技感在里面的哈。

简单说一下实现思路:

首先就是要对后处理有一定基础(场景的基础搭建省略),知道基本的搭建后处理通道的流程:

const composer = new EffectComposer(renderer); // 创建后处理通道
composer.setSize(window.innerWidth, window.innerHeight); // 设置大小
composer.setPixelRatio(window.devicePixelRatio); // 设置像素比
// 渲染通道
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass); // 添加
const v2 = new Three.Vector2(window.innerWidth, window.innerHeight); // 获取当前容器大小,这里我是占满全屏,就用的window
const unrealBloomPass = new UnrealBloomPass(v2, 1.5, 0.5, 0.2); // 创建辉光通道
composer.addPass(unrealBloomPass); // 添加进通道

这里就搭建好了一个基础的后处理器,作用就是让场景里的模型有辉光效果。UnrealBloomPass的参数分别为:

resolution: Three.Vector2, strength: number, radius: number, threshold: number
resolution // 分辨率设置
strength // 定义泛光效果的强度,值越高,明亮的区域越明亮,而且渗入较暗区域的也就越多
radius // 半径设置,越大越大
threshold // 定义阈值,根据场景自定义设置

这里有一些需要注意的点。辉光对场景的背景也会生效(至于原因,我个人愚见是使用GLSL获取了整个屏幕的片元,让后让其中的亮点发光),因此场景最好是黑的,否则:

(renderer.setClearColor(0x00ff00);)

场景也跟着发光很多场合下都不是我们想要的(包括你设置scene.background也是一样,而且设置了还可能会出现一些Bug)。

针对不同的模型(这里是线条,可能不是很明显,底下用原模型进行观察),在使用辉光特效时可能会导致模型变得很模糊,如图:

      

上图中,左边加入了renderTarget,右边没有加入

可以看到,右边比左边光晕更大,模型看起来也要比左边模糊一些。为了解决这个问题,可以在创建EffectComposer时加入WebGLRenderTargert:

const renderTarget = new Three.WebGLRenderTarget(
  window.innerWidth,
  window.innerHeight,
  {
    minFilter: Three.LinearFilter,
    magFilter: Three.LinearFilter,
    format: Three.RGBAFormat,
    colorSpace: Three.SRGBColorSpace, // 重新设定颜色空间
  }
);
// 创建effect后处理器
// const composer = new EffectComposer(renderer);
const composer = new EffectComposer(renderer, renderTarget);

加上即可。如果加不加感觉效果不大的可以不加(比如在加一个描边啥的场景里面,加入描边的模型达到预期,但是其他的模型可能会变得比较模糊,看起来像是眼睛花了)。

2.常见的后处理通道介绍

1.EffectComposer

import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
// 构造函数
EffectComposer(renderer: WebGLRenderer, renderTarget?: WebGLRenderTarget)
  • renderer: 用于渲染场景的渲染器。

  • renderTarget: (可选)EffectComposer内部使用的预配置渲染目标。

EffectComposer的基本工作流程如下:

  1. 创建一个EffectComposer实例。

  2. 添加渲染目标和一系列的Pass。

  3. 在渲染循环中调用composer的render方法。

基础使用:

import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";

const renderTarget = new Three.WebGLRenderTarget(
  window.innerWidth,
  window.innerHeight,
  {
    minFilter: Three.LinearFilter,
    magFilter: Three.LinearFilter,
    format: Three.RGBAFormat,
    colorSpace: Three.SRGBColorSpace, // 重新设定颜色空间
  }
);

const composer = new EffectComposer(renderer, renderTarget); // 后者可选
const renderPass = new RenderPass(scene, camera); // 创建一个渲染通道
composer.addPass(renderPass); // 将渲染通道加到composer中

2.RenderPass

RenderPass通道的作用是把场景和相机作为参数传入,获得场景的渲染结果,不对渲染结果做特定处理。 简单说就是RenderPass用来生成第一张原始图,用来传给后面通道使用,所以一般RenderPass会作为第一个通道

renderToScreen属性:默认值是falsetrue将处理的结果保存到帧缓冲区,false直接显示在canvas画布上面。

(以为我漏了3?其实是辉光的Pass)

4.OutlinePass实现描边发光效果

import { OutlinePass } from "three/examples/jsm/Addons.js";
// 传入长宽、场景、相机
const v2 = new Three.Vector2(container.clientWidth, container.clientHeight);
const outlinePass = new OutlinePass(v2, scene, camera)
// 将此通道结果渲染到屏幕
outlinePass.renderToScreen = true
// OutlinePass相关属性设置
PaoutlinePassss.visibleEdgeColor = new Three.Color(76, 148, 156) // 可见边缘的颜色
outlinePass.hiddenEdgeColor = new Three.Color(0, 1, 0) // 不可见边缘的颜色
outlinePass.edgeGlow = 1.0 // 发光强度
outlinePass.usePatternTexture = false // 是否使用纹理图案
outlinePass.edgeThickness = 2.0 // 边缘浓度
outlinePass.edgeStrength = 2.0  // 边缘的强度,值越高边框范围越大
outlinePass.pulsePeriod = 0 // 闪烁频率,值越大频率越低
// 一般生产环境中会配合鼠标事件,来改变selectedObjects,使选中的物体描边,也可以初始就指定,作为第四个参数传进去
outlinePass.selectedObjects = [需要添加外边框的网格模型]
// 将通道加入组合器
composer.addPass(outlinePass)

5.GlitchPass闪屏效果

import { GlitchPass } from "three/examples/jsm/Addons.js";
// 初始化通道
const glitchPass = new GlitchPass()
// 将此通道结果渲染到屏幕
glitchPass.renderToScreen = true
// 把通道加入到组合器
composer.addPass(glitchPass)

6.FXAAShader和SMAAPass抗锯齿通道

// FXAA抗锯齿Shader
import { FXAAShader } from "three/addons/shaders/FXAAShader.js";
// SMAA抗锯齿通道
import { SMAAPass } from "three/addons/postprocessing/SMAAPass.js";
​
const FXAAPass = new ShaderPass(FXAAShader);
const pixelRatio = renderer.getPixelRatio();
FXAAPass.uniforms.resolution.value.x = 1 / (window.innerWidth * pixelRatio);
FXAAPass.uniforms.resolution.value.y = 1 / (window.innerHeight * pixelRatio);
composer.addPass(FXAAPass);
​
const pixelRatio = renderer.getPixelRatio();
const smaaPass = new SMAAPass(
  window.innerWidth * pixelRatio,
  window.innerHeight * pixelRatio
);
composer.addPass(smaaPass);

二者使用其中的一个就行,个人推荐FXAAShader通道。

7.GammaCorrectionShader伽马校正通道

import { GammaCorrectionShader } from "three/addons/shaders/GammaCorrectionShader.js";
​
// 创建伽马校正通道
const gammaPass = new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaPass);

目前来看,这个通道可以很好校正因为其他shader导致的颜色变化。

8.ShaderPass

作用是将一个自定义的着色器应用到渲染结果上,接收一个着色器(Shader)作为输入,并将其运用到当前的渲染帧上。写法上应该参照的是CopyShader的写法(本身就是用于拷贝已有的渲染结果的!):

import { CopyShader } from "three/examples/jsm/shaders/CopyShader.js"; // 传入了CopyShader着色器,用于拷贝渲染结果
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
​
// 这里展示一些内容,以及自己增加的东西
const shader = {
  uniforms: {
    tDiffuse: { value: null },
    opacity: { value: 1.0 },
    uStrength: {value: 0} // 这个是自己添加的
  },
  vertexShader: `
    varying vec2 vUv;
        void main() {
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
        }
  `,
  fragmentShader: `
    uniform float opacity;
        uniform sampler2D tDiffuse;
        varying vec2 vUv;
        void main() {
            vec4 texel = texture2D( tDiffuse, vUv );
            gl_FragColor = opacity * texel;
            if(vUv.x < 0.5) gl_FragColor = vec4(gl_FragColor.rgb, 0.8); // 这一行是自己添加的,表示左半边的片元透明度都变成了0.8,右半边不变
        }
  `,
};
const shaderPass = new ShaderPass(shader);
composer.addPass(shaderPass);
shaderPass.uniforms.uStrength.value = 2.0; // 调整自定义参数

效果就是:

(左边和右边分庭抗礼)

9.OutputPass

主要作用是将最终的处理结果渲染到屏幕上,一般放在composer的最后一个通道上。(不加好像也没啥大问题)

import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
​
// 添加 OutputPass
const outputPass = new OutputPass();
composer.addPass(outputPass);

3.比较一些Pass的效果

结合这个线条模型,比较在辉光特效下,SMAAPass、Gamma通道、OutputPass之间对结果的影响:

添加了WebGLRenderTarget之后,SMAAPass通道的效果微乎其微(最少这个模型中看不太出来),Gamma通道可以增强效果(而且对于颜色校正也很有帮助),输出Pass也可以增强效果(原因不明)。

总结

以辉光效果通道为引子,整理介绍了一些常见的后处理通道和使用方法,并比较了一些通道对于辉光效果的作用。

别怕懒,多尝试,总归能找到比较合适的!!!

(PS: 有讲的不对的不好的地方,欢迎指正!!)

(PPS: 坚持学习,做大做强)

PPPS:附上源码:

<template>
  <div id="bloom" ref="bloomRef"></div>
  <div id="btnBox">
    <button @click="handleAddPass">增加SMAA</button>
    <button @click="handleRemovePass">移除SMAA</button>
    <button @click="handleAddGamma">增加Gamma</button>
    <button @click="handleRemoveGamma">移除Gamma</button>
    <button @click="handleAddOutput">增加Output</button>
    <button @click="handleRemoveOutput">移除Output</button>
  </div>
</template>

<script lang="ts" setup>
import * as Three from "three";
import { ref, onMounted } from "vue";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/Addons.js";
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
import {
  SMAAPass, // 抗锯齿通道
  OutputPass, // 输出通道
  UnrealBloomPass, // 泛光通道,感觉这个只能在暗色的场景里面使用,针对明亮的物体,发光
  ShaderPass, // 自定义shader通道
  RenderPass, // 渲染通道
  GammaCorrectionShader, // 伽马颜色校正后处理Shader
} from "three/examples/jsm/Addons.js";

const bloomRef = ref();

const scene = new Three.Scene();

const camera = new Three.PerspectiveCamera(
  80,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.set(0, 80, 100);
camera.lookAt(0, 0, 0);

const ambientLight = new Three.AmbientLight(0xffffff, 2);
scene.add(ambientLight);

const createLine = (model: Three.Mesh) => {
  const edges = new Three.EdgesGeometry(model.geometry, 20);
  const line = new Three.LineSegments(
    edges,
    new Three.LineBasicMaterial({
      color: new Three.Color(0x00bfff),
    })
  );
  const quar = new Three.Quaternion();
  const pos = new Three.Vector3();
  const scale = new Three.Vector3();
  model.getWorldPosition(pos);
  model.getWorldQuaternion(quar);
  model.getWorldScale(scale);
  line.scale.copy(scale);
  line.position.copy(pos);
  line.quaternion.copy(quar);
  return line;
};

new GLTFLoader().load("/manipulator.glb", (glb) => {
  const model = glb.scene.children[0];
  const lightGroup = new Three.Group();
  model.traverse((child) => {
    if (child.isMesh) {
      const line = createLine(child);
      lightGroup.add(line);
    }
  });
  scene.add(lightGroup);
  lightGroup.scale.set(20, 20, 20);
});

const renderer = new Three.WebGLRenderer({
  antialias: true,
  logarithmicDepthBuffer: true,
});
renderer.setPixelRatio(window.devicePixelRatio); // 设置设备像素比,避免渲染模糊问题
const renderTarget = new Three.WebGLRenderTarget(
  window.innerWidth,
  window.innerHeight,
  {
    minFilter: Three.LinearFilter,
    magFilter: Three.LinearFilter,
    format: Three.RGBAFormat,
    colorSpace: Three.SRGBColorSpace, // 重新设定颜色空间
  }
);

// 创建effect后处理器
// const composer = new EffectComposer(renderer);
const composer = new EffectComposer(renderer, renderTarget);
composer.setSize(window.innerWidth, window.innerHeight); // 设置大小
composer.setPixelRatio(window.devicePixelRatio); // 设置像素比
// 渲染通道
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass); // 添加
const v2 = new Three.Vector2(window.innerWidth, window.innerHeight);
const unrealBloomPass = new UnrealBloomPass(v2, 1.5, 0.5, 0);
nrealBloomPass.renderToScreen = true;
composer.addPass(unrealBloomPass);
// 创建伽马颜色矫正通道
const gammaPass = new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaPass);

const shader = {
  uniforms: {
    tDiffuse: { value: null },
    opacity: { value: 1.0 },
  },
  vertexShader: `
    varying vec2 vUv;
		void main() {
			vUv = uv;
			gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
		}
  `,
  fragmentShader: `
    uniform float opacity;
		uniform sampler2D tDiffuse;
		varying vec2 vUv;
		void main() {
			vec4 texel = texture2D( tDiffuse, vUv );
			gl_FragColor = opacity * texel;
      if(vUv.x < 0.5) gl_FragColor = vec4(gl_FragColor.rgb, 0.8);
		}
  `,
};
const shaderPass = new ShaderPass(shader);
// composer.addPass(shaderPass);
// 创建SMAA抗锯齿通道
const pixelRatio = renderer.getPixelRatio();
const smaaPass = new SMAAPass(
  window.innerWidth * pixelRatio,
  window.innerHeight * pixelRatio
);
composer.addPass(smaaPass);
// 创建输出通道,防止点击发光后场景颜色变暗
const outputPass = new OutputPass();
composer.addPass(outputPass);

const handleAddPass = () => {
  composer.addPass(smaaPass);
  console.log(composer.passes);
};
const handleRemovePass = () => {
  composer.removePass(smaaPass);
  console.log(composer.passes);
};
const handleAddGamma = () => {
  composer.addPass(gammaPass);
  console.log(composer.passes);
};
const handleRemoveGamma = () => {
  composer.removePass(gammaPass);
  console.log(composer.passes);
};
const handleAddOutput = () => {
  composer.addPass(outputPass);
  console.log(composer.passes);
};
const handleRemoveOutput = () => {
  composer.removePass(outputPass);
  console.log(composer.passes);
};

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);

const loop = () => {
  composer.render();
  requestAnimationFrame(loop);
};

onMounted(() => {
  renderer.setSize(bloomRef.value.clientWidth, bloomRef.value.clientHeight);
  bloomRef.value.appendChild(renderer.domElement);
  loop();
});
</script>

<style lang="scss" scoped>
#bloom {
  width: 100vw;
  height: 100vh;
}
#btnBox {
  position: fixed;
  top: 20px;
  right: 50px;
  cursor: pointer;
}
</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值