Three.js教程
入门
EffectComposer选中效果

EffectComposer 实现选中效果

本文将展示如何在 Three.js 中通过后期处理(post-processing)的方式,为鼠标悬停到的物体添加高亮轮廓线条(OutlinePass)。

基础概念介绍

EffectComposer

在 Three.js 中,EffectComposer 是一个用于管理后期处理(post-processing)流程的核心类。它可以帮助我们将一系列后期处理的“通道”(Pass)有序地拼接起来,从而在场景的最终渲染结果上叠加各种视觉特效(例如模糊、泛光、描边、抗锯齿等)。

1. 为什么需要后期处理?

在一般的 3D 渲染流程中,Three.js 会直接把场景(Scene)和相机(Camera)渲染到屏幕上,这种方式可以满足大部分基本需求。但如果想添加一些高级的视觉效果,比如:

  • 对场景进行模糊处理(Blur Pass)。
  • 实现高光泛光(Bloom Pass)。
  • 对场景进行边缘描边(Outline Pass)。
  • 实现像素化或其他风格化效果。

我们就需要先得到一张完整的场景图,然后对这张图做各种“后期处理”,再把处理后的结果显示在屏幕上。EffectComposer 提供了这样的多通道流程管理。

2. EffectComposer 的工作原理

可以把 EffectComposer 想象成一条“后期处理流水线”:

  1. 首先使用 RenderPass 将场景正常渲染到一个中间缓冲(Render Target)中;
  2. 再依次将其他特效通道(Pass)应用到这个缓冲里,每个 Pass 都会读入上一阶段的图像并输出到新的缓冲或直接覆盖上一缓冲;
  3. 最终将处理完成的结果呈现到屏幕上。

在编程层面,EffectComposer 会在幕后管理多个 WebGLRenderTarget,负责把一个 Pass 的输出当作下一个 Pass 的输入,这样就能层层叠加、组合各项后期特效。通过这种“管线”式的处理,你可以灵活地启用或禁用某些 Pass,也可以按需求调整它们的顺序。

3. 常见的 Pass 介绍

  • RenderPass:最基础的渲染通道,用来把正常的场景渲染到后期处理管线的输入中,如果没有它,就得不到初始的完整场景图像。
  • OutlinePass:为指定的物体添加轮廓描边效果,通常结合鼠标拾取(Raycaster)实现“鼠标悬停高亮”。
  • UnrealBloomPass:利用泛光/发光(Bloom)算法,为场景中亮度较高的像素添加炫光特效。
  • FilmPass / GlitchPass:为画面添加胶片粒子效果或故障失真效果,常用于特定风格的渲染或过渡动画。
  • SSAOPass / FXAAPass:分别是屏幕空间环境光遮蔽(SSAO)和快速抗锯齿(FXAA)的后期处理通道。

这些通道都可以根据自己的需求选择添加,或自行编写自定义的后期 ShaderPass,实现更特殊的效果。

4. EffectComposer 的基本使用步骤

下面以一个最简单的使用场景为例,演示如何设置后期处理。

  1. 创建 EffectComposer 实例

    const composer = new EffectComposer(renderer);
  2. 添加 RenderPass(必不可少,用于初次渲染场景)

    const renderPass = new RenderPass(scene, camera);
    composer.addPass(renderPass);
  3. 添加其他想要的后期 Pass

    const outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera);
    composer.addPass(outlinePass);
  4. 在动画循环中渲染

    function animate() {
      requestAnimationFrame(animate);
      // ... 更新相机或控制器 ...
      composer.render(); // 使用 composer 而不是 renderer.render(scene, camera)
    }

在完成上述步骤后,每一帧的渲染不再直接输出到屏幕,而是先通过 Composer 管线,逐步叠加各个 Pass 的效果。

5. Pass 的执行顺序

EffectComposer 中,Pass 的执行顺序就是你添加 Pass 的顺序,addPass() 越靠前,它越先执行。

  • 例如:先对场景做 SSAO Pass,接着做 Outline Pass,最后做 Bloom Pass,就可能渲染出“具有环境光遮蔽 + 描边 + 泛光” 的合成效果。
  • 如果交换其中任意 Pass 的顺序,画面结果也会不同。因此在配置多重 Pass 时,要根据需求和效果调试顺序和参数。

代码步骤

1. 项目环境准备

安装依赖:

# 使用 npm
npm install three

引入依赖:

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer";
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass";
import { OutlinePass } from "three/examples/jsm/postprocessing/OutlinePass";

在这个示例中,我们还需要以下额外的模块:

  1. OrbitControls:用于场景的鼠标旋转、缩放、平移等交互控制。
  2. EffectComposerRenderPassOutlinePass:Three.js 的后期处理管线相关模块,用来实现轮廓线高亮效果。

2. 创建场景(Scene)

// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x333333);
  • Scene:Three.js 中所有对象都要加入到场景里,才能通过相机拍摄并渲染出来。
  • scene.background:设置场景的背景色。

3. 创建相机(Camera)

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
  • PerspectiveCamera:透视相机,模拟人眼看到的透视效果。
  • 参数含义:
    1. 视野角(field of view, FOV):75 度。
    2. 宽高比(aspect)window.innerWidth / window.innerHeight
    3. 近截面(near):0.1。
    4. 远截面(far):1000。
  • camera.position.z = 5:将相机往场景 Z 轴正方向移动。

4. 创建渲染器(Renderer)

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
  • WebGLRenderer:使用 WebGL 在 <canvas> 上进行渲染。
  • renderer.setSize(...):设置渲染器的尺寸。通常我们让它和浏览器窗口一样大。
  • renderer.domElement:这是渲染器内部生成的 <canvas> 元素,把它添加到页面 body 中,就能看到画面了。

5. 轨道控制器(OrbitControls)

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
  • OrbitControls:可以使用鼠标拖拽、滚轮缩放以及键盘操作来移动和旋转相机,便于查看场景中的物体。
  • enableDamping:开启时可以让相机的旋转和缩放带有一定的缓动效果,体验更好。

6. 效果合成器(EffectComposer)与后期处理

const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
  • EffectComposer:后期处理的核心,用于将多个 Pass(后期处理阶段)合并在一起依次处理。
  • RenderPass:最基础的渲染通道,会先把场景正常渲染出来,然后再给其他效果 Pass 进行处理。

6.1 添加轮廓线特效(OutlinePass)

const outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera);
outlinePass.edgeStrength = 3; // 边缘强度
outlinePass.edgeGlow = 1; // 发光强度
outlinePass.edgeThickness = 1; // 边缘厚度
outlinePass.pulsePeriod = 0; // 脉冲周期
outlinePass.visibleEdgeColor.set("#ffffff"); // 可见边缘颜色
outlinePass.hiddenEdgeColor.set("#190a05"); // 被遮挡边缘的颜色
composer.addPass(outlinePass);
  • OutlinePass:专门用来渲染指定物体的高亮边缘。
  • 主要参数解释:
    1. edgeStrength:边缘明亮程度。
    2. edgeGlow:边缘发光程度。
    3. edgeThickness:边缘线条厚度。
    4. pulsePeriod:如果想让边缘出现周期性的脉冲,可以设置一个大于 0 的数值。
    5. visibleEdgeColor:可见边缘的颜色。
    6. hiddenEdgeColor:当物体边缘被自身或其他几何体遮挡时的线条颜色。

outlinePass 加入到 composer 后,每一帧渲染时都会先经过 RenderPass 的正常渲染,然后再渲染出高亮轮廓效果。

7. 创建几何体(Geometries)与材质

示例中我们创建了 3 种简单几何体——立方体、球体、圆锥体,并用不同的颜色区分:

const geometries = [new THREE.BoxGeometry(), new THREE.SphereGeometry(0.5, 32, 32), new THREE.ConeGeometry(0.5, 1, 32)];
 
const materials = [
  new THREE.MeshPhongMaterial({ color: 0xff0000 }),
  new THREE.MeshPhongMaterial({ color: 0x00ff00 }),
  new THREE.MeshPhongMaterial({ color: 0x0000ff }),
];
 
const meshes = [];
 
// 创建多个物体并随机分布
for (let i = 0; i < 3; i++) {
  const mesh = new THREE.Mesh(geometries[i], materials[i]);
  mesh.position.set((Math.random() - 0.5) * 4, (Math.random() - 0.5) * 4, (Math.random() - 0.5) * 4);
  scene.add(mesh);
  meshes.push(mesh);
}
  • BoxGeometry / SphereGeometry / ConeGeometry:Three.js 提供的一些内置几何体类,轻松生成基础形状。
  • MeshPhongMaterial:支持光照的材质类型,可以和灯光配合呈现出明暗效果。
  • 随机位置:通过 (Math.random() - 0.5) * 4 简单产生一个 -2 到 2 之间的随机位置,从而让几个物体在视野内散布开来。

8. 添加灯光(Lights)

const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
 
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);
  • AmbientLight:环境光,提供一个基础的全局照明,让物体不会因为背面没有光照而全黑。
  • DirectionalLight:方向光(类似太阳光),位置非常远,光线方向平行。
  • 通过控制它们的强度可以让物体有更好的高光阴影效果。

9. 鼠标事件与射线检测(Raycaster)

要让鼠标指向的物体高亮,最关键的一步就是使用射线检测(Raycaster)判断当前鼠标在 3D 空间中“碰撞”到哪个物体。

  1. 新建射线投射器、向量存鼠标坐标:

    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();
  2. mousemove 事件中,更新鼠标在设备坐标系(NDC)中的位置,并使用 Raycaster 做拾取检测:

    function onMouseMove(event) {
      // 将鼠标位置归一化为设备坐标。x 和 y 的范围是 (-1 to +1)
      mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
      mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
     
      // 更新射线投射器:从相机到鼠标所在点
      raycaster.setFromCamera(mouse, camera);
     
      // 计算射线与物体的焦点
      const intersects = raycaster.intersectObjects(meshes);
     
      if (intersects.length > 0) {
        // 选中第一个相交的物体
        outlinePass.selectedObjects = [intersects[0].object];
      } else {
        // 没有相交的物体,则清空选中
        outlinePass.selectedObjects = [];
      }
    }
    • raycaster.intersectObjects(meshes) 会返回与所有传入物体相交的集合,包含距离信息、相交点等;如果鼠标没有指向任何物体,返回空数组。
    • selectedObjectsOutlinePass 中的一个数组。只要把想要高亮的物体放进这个数组,它就会对这些物体的边缘进行描边渲染。
  3. 把事件监听器加到 window 上:

    window.addEventListener("mousemove", onMouseMove);

10. 处理窗口大小变化

function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
 
  renderer.setSize(window.innerWidth, window.innerHeight);
  composer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener("resize", onWindowResize);
  • 当浏览器窗口大小变化时,需要更新相机的 aspect,并且重新设置渲染器和后期处理器的大小。否则画面比例会失真或变形。

11. 动画循环(Render Loop)

function animate() {
  requestAnimationFrame(animate);
  controls.update();
  composer.render(); // 使用 composer 来进行后期处理渲染
}
 
animate();
  • requestAnimationFrame:浏览器提供的 API,每帧调用一次 animate()
  • controls.update():持续更新 OrbitControls,让相机交互生效。
  • composer.render():在每一帧使用 EffectComposer 渲染场景,并带有后期处理的效果(包括 OutlinePass)。

到此,所有核心逻辑已完成。

代码

https://github.com/calmound/threejs-demo/tree/main/select (opens in a new tab)