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 想象成一条“后期处理流水线”:
- 首先使用 RenderPass 将场景正常渲染到一个中间缓冲(Render Target)中;
- 再依次将其他特效通道(Pass)应用到这个缓冲里,每个 Pass 都会读入上一阶段的图像并输出到新的缓冲或直接覆盖上一缓冲;
- 最终将处理完成的结果呈现到屏幕上。
在编程层面,EffectComposer 会在幕后管理多个 WebGLRenderTarget,负责把一个 Pass 的输出当作下一个 Pass 的输入,这样就能层层叠加、组合各项后期特效。通过这种“管线”式的处理,你可以灵活地启用或禁用某些 Pass,也可以按需求调整它们的顺序。
3. 常见的 Pass 介绍
- RenderPass:最基础的渲染通道,用来把正常的场景渲染到后期处理管线的输入中,如果没有它,就得不到初始的完整场景图像。
- OutlinePass:为指定的物体添加轮廓描边效果,通常结合鼠标拾取(Raycaster)实现“鼠标悬停高亮”。
- UnrealBloomPass:利用泛光/发光(Bloom)算法,为场景中亮度较高的像素添加炫光特效。
- FilmPass / GlitchPass:为画面添加胶片粒子效果或故障失真效果,常用于特定风格的渲染或过渡动画。
- SSAOPass / FXAAPass:分别是屏幕空间环境光遮蔽(SSAO)和快速抗锯齿(FXAA)的后期处理通道。
这些通道都可以根据自己的需求选择添加,或自行编写自定义的后期 ShaderPass,实现更特殊的效果。
4. EffectComposer 的基本使用步骤
下面以一个最简单的使用场景为例,演示如何设置后期处理。
-
创建 EffectComposer 实例
const composer = new EffectComposer(renderer);
-
添加 RenderPass(必不可少,用于初次渲染场景)
const renderPass = new RenderPass(scene, camera); composer.addPass(renderPass);
-
添加其他想要的后期 Pass
const outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera); composer.addPass(outlinePass);
-
在动画循环中渲染
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";
在这个示例中,我们还需要以下额外的模块:
OrbitControls
:用于场景的鼠标旋转、缩放、平移等交互控制。EffectComposer
、RenderPass
、OutlinePass
: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:透视相机,模拟人眼看到的透视效果。
- 参数含义:
- 视野角(field of view, FOV):75 度。
- 宽高比(aspect):
window.innerWidth / window.innerHeight
。 - 近截面(near):0.1。
- 远截面(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:专门用来渲染指定物体的高亮边缘。
- 主要参数解释:
- edgeStrength:边缘明亮程度。
- edgeGlow:边缘发光程度。
- edgeThickness:边缘线条厚度。
- pulsePeriod:如果想让边缘出现周期性的脉冲,可以设置一个大于 0 的数值。
- visibleEdgeColor:可见边缘的颜色。
- 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 空间中“碰撞”到哪个物体。
-
新建射线投射器、向量存鼠标坐标:
const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2();
-
在
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)
会返回与所有传入物体相交的集合,包含距离信息、相交点等;如果鼠标没有指向任何物体,返回空数组。- selectedObjects:
OutlinePass
中的一个数组。只要把想要高亮的物体放进这个数组,它就会对这些物体的边缘进行描边渲染。
-
把事件监听器加到 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)