简介:基于Three.js的粒子特效网页设计源码,面向前端开发者和三维可视化初学者,演示如何用JavaScript与CSS构建动态粒子系统并集成3D模型,适用于个人网站、产品展示等需要增强视觉冲击力的场景。资源共41个文件,容量约6.85MB,涵盖17个PNG图片、10个JS脚本、3个GLB模型、2个WASM文件、2个SVG矢量图,以及HTML、CSS、JSON等前端配套文件;其中JS脚本负责粒子生成与交互,GLB/WASM用于加载与解码三维模型,图片和样式则完成视觉表现与页面布局。已有128人学习浏览,可快速体验Three.js粒子特效的完整实现。项目目录结构清晰,包含依赖管理配置,便于本地还原运行环境;借助示例模型与解码逻辑,可深入理解粒子系统、模型加载和前端动画交互的结合方式,适合作为网页特效开发的入门素材与灵感来源。
1. 粒子特效在网页设计里到底解决什么问题
打开一个汽车品牌官网,首屏背景里那些缓慢漂移的发光星点,往往不是视频,而是浏览器实时计算的粒子系统。粒子特效的价值在于:用极低的素材成本给页面增加一层动态质感,让空白的网页背景不再是纯色或静态图片,而是带呼吸感的视觉氛围。Three.js是完成这件事最顺手的工具,它把WebGL复杂的矩阵、缓冲区和着色器管线封装成场景、相机、几何体等直觉概念,前端工程师只需要管理对象属性和动画循环,就能做出过去需要图形学背景才能实现的粒子效果。如果你已经会写基础JavaScript,想在网页里加入粒子背景、交互动效或者表白彩蛋,这篇内容可以直接照着落地。
2. Three.js粒子系统核心:Points、BufferGeometry与着色器
2.1 为什么选Three.js而不是从零写WebGL
纯WebGL渲染一个粒子需要经过获取上下文、编译着色器、创建VBO、绑定属性、启用顶点数组、上传uniform等十几步,光是初始化就超过百行,而且每一步出错都只会得到黑屏和控制台里一行难懂的代码。Three.js把这一整套流程压缩成三个对象:场景(Scene)、相机(Camera)、渲染器(Renderer),而粒子本身则用Points来表示。
用原生WebGL画100个点,需要自己管理一个Float32Array、写两个着色器、处理窗口大小变化;用Three.js则是创建BufferGeometry、绑定位置属性、设置材质、加入场景,一共四五行代码。对于做网页设计源码而不是图形学底座的场景,Three.js的抽象层把“调效果”和“写管线”彻底分开,你可以在浏览器实时调试粒子颜色、大小、运动速度,而不需要重新编译着色器。这也是为什么很多开源粒子特效源码都基于Three.js写,而不是基于原生WebGL。
2.2 Points与BufferGeometry的组织方式
在Three.js里,粒子系统并不是由独立的小球物体组成的,而是把所有粒子位置放进同一个几何体BufferGeometry中,再用Points渲染。每个粒子的本质是几何体里的一个顶点,顶点的x、y、z坐标就是粒子在三维空间中的位置,而一个顶点的其他属性,比如颜色、大小、纹理坐标,也可以作为单独的BufferAttribute存储在几何体上。
一个创建10个粒子点云的最小代码:
const positions = new Float32Array(30); // 10个粒子,每个粒子占xyz三个分量 for (let i = 0; i < 30; i++) { positions[i] = (Math.random() - 0.5) * 10; // 范围控制在[-5, 5] } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const material = new THREE.PointsMaterial({ size: 0.15, color: 0xffffff, transparent: true, opacity: 0.8 }); const points = new THREE.Points(geometry, material); scene.add(points);Float32Array(30)中的30是三个分量乘以10个粒子得到的。setAttribute('position', ...)告诉Three.js:每个顶点有三组数值,这组数据构成粒子的位置。Points渲染时不会像Mesh那样组装三角形,而是把每个顶点画成一个点,点的尺寸由材质中的size控制。
2.3 着色器决定粒子的最终长相
PointsMaterial适合快速出效果,但它的能力有限。当你希望粒子大小随距离变化、颜色随高度渐变、或者每个粒子有不同透明度时,需要换成ShaderMaterial自己编写着色器。Three.js会在背后把标准uniform(比如modelViewMatrix、projectionMatrix)和attribute(position)注入到着色器代码中。
一个最简单的顶点着色器可以让粒子自带“近大远小”的透视效果:
void main() { vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_PointSize = (300.0 / -mvPosition.z) * 0.5; gl_Position = projectionMatrix * mvPosition; }mvPosition.z是粒子到相机的深度,-mvPosition.z永远为正数,粒子离相机越远,分母越大,gl_PointSize越小。这个公式是粒子系统里的常见做法,直接设置gl_PointSize可以避免所有粒子在屏幕上显示成同样大小,让视角更真实。片元着色器则控制每个点最终绘制的颜色和透明渐变,如果你不给它,Three.js会默认填充白色。
PointsMaterial和ShaderMaterial的适用边界很清晰:
| 材质类型 | 适用场景 | 灵活性 | 性能 |
|---|---|---|---|
| PointsMaterial | 静态点云、背景粒子、快速原型 | 低,只能改颜色、大小、透明度 | 较高,内置优化 |
| ShaderMaterial | 动态飘动、鼠标交互、颜色渐变 | 高,可以控制每个顶点的生命周期 | 取决于着色器复杂度 |
3. 用Three.js落地一个可运行的粒子特效页面
3.1 搭建页面骨架并引入Three.js
不需要webpack或vite,直接用浏览器模块化就可以跑通。在HTML里用importmap指定three模块的地址,业务代码用<script type="module">引入,这段代码可以原样保存成一个index.html运行。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>基于Three.js的粒子特效网页设计源码</title> <style> body { margin: 0; overflow: hidden; } canvas { display: block; } </style> </head> <body> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three/build/three.module.js" } } </script> <script type="module"> // 粒子特效逻辑 </script> </body> </html>这段骨架的关键是importmap。它让浏览器在解析import * as THREE from 'three'时,把three映射到CDN上的模块文件。不同版本API基本一致,实际使用时建议固定一个明确版本号,避免CDN默认版本更新导致源码行为改变。overflow: hidden防止粒子飘出视口时出现滚动条。
3.2 初始化场景、相机与渲染器
在<script type="module">中,第一步是创建三件套。这里有一个设计上的选择:让Canvas背景透明,这样粒子层可以覆盖在页面内容上方,也可以作为CSS背景层垫在文字下面。
const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 1000); camera.position.z = 25; const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(innerWidth, innerHeight); renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); document.body.appendChild(renderer.domElement);PerspectiveCamera第一个参数是视角角度,60度接近人眼自然视野;第二个参数是宽高比,窗口变化时需要同步更新。antialias: true会让粒子边缘更平滑,但会消耗性能。alpha: true是让渲染器保留画布透明背景,方便后面叠加网页文字和图片。setPixelRatio(Math.min(devicePixelRatio, 2))限制像素比最大为2,因为iPhone等设备devicePixelRatio可能达到3,超过2对画面提升有限,但GPU负担会明显上升。
3.3 创建粒子几何体并绑定材质
3.3.1 生成粒子坐标并创建BufferGeometry
这一步是源码的核心,粒子数量、分布范围、颜色都集中在这里调整。先用1500个粒子做一个球旋转效果,分布范围控制在radius半径内。
const count = 1500; const radius = 10; const positions = new Float32Array(count * 3); for (let i = 0; i < count; i++) { // 生成均匀分布在球体内的坐标 const theta = Math.random() * Math.PI * 2; const phi = Math.acos((Math.random() * 2) - 1); const r = radius * Math.cbrt(Math.random()); const x = r * Math.sin(phi) * Math.cos(theta); const y = r * Math.sin(phi) * Math.sin(theta); const z = r * Math.cos(phi); positions[i * 3] = x; positions[i * 3 + 1] = y; positions[i * 3 + 2] = z; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));r * Math.cbrt(Math.random())这一行很多人直接写成radius * Math.random(),这会导致粒子在球心附近密度远高于球面。由于体积和半径是三次方关系,用cbrt取立方根,才能让粒子在球体内部均匀分布。phi和theta是球坐标的两个角度,配合三角函数转成三维坐标。
3.3.2 用PointsMaterial控制颜色和大小
绑定材质时,可以让粒子呈现单一颜色,也可以用vertexColors让每个粒子带独立颜色。这里先用最简单的方案,把粒子做成浅蓝到白色的渐变,通过additive混合实现发光效果。
const material = new THREE.PointsMaterial({ color: 0x66ccff, size: 0.12, transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending, depthWrite: false }); const points = new THREE.Points(geometry, material); scene.add(points);AdditiveBlending是粒子特效的经典选项,多个粒子叠加后亮度增加,产生光晕感。depthWrite: false让粒子不写入深度缓冲,否则粒子前后遮挡关系会导致后面的粒子被直接剔除,透明效果会很难看。size: 0.12表示粒子在屏幕上的像素尺寸,这个值需要根据相机距离和浏览器窗口不断调整,如果电脑上看着像针眼,就加大到0.2。
3.4 动画循环与页面自适应
粒子就位后,用一个requestAnimationFrame循环让整个粒子球缓慢旋转。旋转不是必须的,但旋转能给静态粒子增加动态感,这是粒子特效网页源码里最常见的默认动效。
function animate() { requestAnimationFrame(animate); points.rotation.y += 0.002; points.rotation.x += 0.001; renderer.render(scene, camera); } animate(); window.addEventListener('resize', () => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight); });points.rotation.y += 0.002每次帧加0.002弧度,60帧每秒时大约每帧转0.115度,转完整圈需要约52秒,这个速度不会让用户眩晕。resize事件里必须执行camera.updateProjectionMatrix(),否则窗口拉伸后画面会扭曲。你也可以在animate中根据鼠标位置动态调整旋转速度,为后续交互做准备。
4. 粒子特效的进阶玩法与性能调优
4.1 用ShaderMaterial做动态粒子
PointsMaterial最大的限制是没有时间概念,不能让粒子自己动。改成ShaderMaterial后,可以在顶点着色器里读取一个uniform变量uTime,对每个粒子的位置叠加正弦波,实现波浪飘动效果。这种手法在three.js教程里常被用来做星空、烟花、光雨。
const vertexShader = ` uniform float uTime; void main() { vec3 p = position; p.y += sin(uTime + p.x * 2.0) * 0.3; p.x += cos(uTime + p.z * 2.0) * 0.2; vec4 mvPosition = modelViewMatrix * vec4(p, 1.0); gl_PointSize = (300.0 / -mvPosition.z) * 0.4; gl_Position = projectionMatrix * mvPosition; } `; const material = new THREE.ShaderMaterial({ uniforms: { uTime: { value: 0 } }, vertexShader, fragmentShader: ` void main() { gl_FragColor = vec4(0.4, 0.8, 1.0, 0.85); } `, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false });动画循环里更新uniform:material.uniforms.uTime.value += 0.02;。uTime是每帧递增的累计时间,sin和cos让它产生周期性上下左右波动。这里的p.x * 2.0是频率系数,系数越大,粒子在空间上抖动越频繁,看起来越“碎”。效果相当于在原有粒子位置上叠加一层波浪扰动,既保持整体分布,又模拟了粒子群体的呼吸感。
4.2 用Canvas纹理让粒子不再是方块
默认粒子渲染为正方形,显存不够通透。用一张圆形渐变的Canvas纹理替换掉粒子形状,是提升质感成本最低的操作。在网页设计美化里,这一步能立刻让粒子从“马赛克”变成“光斑”。
function createParticleTexture() { const canvas = document.createElement('canvas'); canvas.width = 64; canvas.height = 64; const ctx = canvas.getContext('2d'); const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32); gradient.addColorStop(0, 'rgba(255,255,255,1)'); gradient.addColorStop(0.6, 'rgba(255,255,255,0.6)'); gradient.addColorStop(1, 'rgba(255,255,255,0)'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, 64, 64); return new THREE.CanvasTexture(canvas); }把纹理赋给PointsMaterial的map属性,并把transparent设为true:
const material = new THREE.PointsMaterial({ size: 0.2, map: createParticleTexture(), transparent: true, blending: THREE.AdditiveBlending, depthWrite: false });使用Canvas纹理的优势在于完全离线生成,不需要等待图片加载,也不会因为引入外部资源出现跨域问题。径向渐变中间的白色高亮,边缘透明,叠加AdditiveBlending发光感会很强。
4.3 鼠标交互:让粒子围绕光标聚散
交互是粒子特效源码中最容易让人感到惊喜的部分。做法是在ShaderMaterial中传入一个uMouse,顶点着色器计算粒子到鼠标的距离,把距离不远的粒子往鼠标方向拉一下,形成凹陷或凸起。
let mouse = new THREE.Vector2(10, 10); window.addEventListener('pointermove', (e) => { mouse.x = (e.clientX / innerWidth) * 2 - 1; mouse.y = -(e.clientY / innerHeight) * 2 + 1; }); const material = new THREE.ShaderMaterial({ uniforms: { uTime: { value: 0 }, uMouse: { value: mouse } }, vertexShader: ` uniform vec2 uMouse; void main() { vec3 p = position; float dist = distance(p.xz, uMouse * 8.0); p.y += smoothstep(2.0, 0.0, dist) * 0.8; // ...后续gl_PointSize计算 } `, ... });这里uMouse由归一化坐标转换而来,uMouse * 8.0是把鼠标位置映射到粒子所在的10单位半径范围内。smoothstep(2.0, 0.0, dist)表示当dist小于2时,返回值从0平滑过渡到1,越靠近鼠标的粒子被抬得越高,形成“粒子跟随光标鼓起”的效果。如果改成负数,就变成凹坑。
4.4 性能优化:从粒子数量到渲染优化
粒子虽轻,但数量上去了同样会拖垮帧率。比较稳妥的负载区间和经验参数如下:
| 参数 | 桌面端推荐值 | 移动端推荐值 | 说明 |
|---|---|---|---|
| 粒子数量 | 5000 - 20000 | 1000 - 5000 | 超过2万时动画循环CPU开销明显增加 |
| 粒子size | 0.1 - 0.2 | 0.1 - 0.3 | 过大反而看着像噪点 |
| 像素比 | 1 - 2 | 1 | 移动端建议固定为1 |
| antialias | 开启 | 关闭 | 移动端关闭可省出可观的GPU占用 |
| 阴影 | 不使用 | 不使用 | 粒子系统一般不开阴影 |
如果要在动画中持续修改顶点位置,需要在初始化时标记为动态缓冲,避免每次都重建GPU缓冲区:
geometry.getAttribute('position').usage = THREE.DynamicDrawUsage();更新位置后还要手动把needsUpdate置为true:
const posAttr = geometry.getAttribute('position'); // 修改posAttr.array里的值 posAttr.needsUpdate = true;这个细节很容易被忽略。不加usage时Three.js默认使用静态缓冲,但动态更新同样能工作,只是每次更新会产生较大性能损耗。对于超过5000粒子的场景,标记DynamicDrawUsage后帧率提升非常明显。
5. 把粒子特效放进真实网页设计的三个关键步骤
5.1 处理粒子背景与页面内容的层级
直接把粒子Canvas追加到body里,会盖住所有文字和按钮。常见的做法是让Canvas固定定位在页面底层,给内容容器设置更高的z-index,并让Canvas不响应鼠标事件。
#particle-canvas { position: fixed; top: 0; left: 0; z-index: -1; pointer-events: none; } .page-content { position: relative; z-index: 1; }z-index: -1让Canvas成为页面背景,内容自动浮在上层。position: fixed保证滚动页面时粒子背景始终固定在视口内,不会跟随滚动条滚走。如果你希望粒子跟随页面滚动,可以改成position: absolute放在首屏容器内。
5.2 移动端适配:动态降低粒子数量与像素比
移动端GPU能力差别很大,不能用桌面端粒子数量直接跑。在初始化粒子之前,可以先根据设备给数量打折:
const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent); const baseCount = isMobile ? 1200 : 5000; const pixelRatio = isMobile ? 1 : Math.min(devicePixelRatio, 2); renderer.setPixelRatio(pixelRatio);动态修改粒子数量不能直接改变count,常见做法是在初始化前确定好数量,或者重建几何体。对于粒子背景,一般不需要实时调整,而是在页面加载时判断一次,避免频繁重建带来的性能浪费。
5.3 用浏览器工具验证效果与内存占用
集成stats.js是最快的帧率验证方式,它能把每帧渲染时间显示在屏幕角落。也可以直接使用Chrome DevTools的Performance录制,观察渲染一帧的耗时是否稳定在16毫秒以内。内存泄漏的排查看Performance Monitor里的JS heap大小,如果持续上涨,检查是否在animate循环里反复创建了新的BufferAttribute或纹理。
最后留一个小技巧:在页面躲藏时暂停动画循环,可以大幅降低后台标签页的CPU消耗:
document.addEventListener('visibilitychange', () => { if (document.hidden) { cancelAnimationFrame(animationId); } else { animate(); } });把requestAnimationFrame的返回ID存为animationId,页面重新可见时继续循环。这个优化对粒子背景尤其重要,因为用户切出标签页后,粒子动画还在白白消耗GPU资源。
本文还有配套的精品资源,点击获取