Three.js粒子系统实现3D烟花秀
2026/9/10 7:52:51 网站建设 项目流程

简介:这是一份轻量级的新年3D跨年烟花秀前端实现代码,面向Web前端初学者与节日主题项目开发者,解决节日氛围网页快速部署需求。资源以HTML+JavaScript双文件构成(1个HTML主页面、1个JS核心逻辑脚本),总大小仅10KB,结构极简,无需依赖外部库即可在浏览器中直接运行3D粒子烟花效果;其中index.js封装了基于Canvas的粒子系统、时间轴控制与倒计时触发机制,index.html提供基础容器与样式,便于二次定制烟花颜色、轨迹或音效。目前已有912人学习下载,适合用于个人博客新年专题页、企业内网贺岁屏、教学演示案例等场景。读者可直接运行体验立体烟花绽放效果,深入理解粒子系统建模、帧循环渲染及交互式倒计时集成等Web图形编程核心实践。

1. 用 Three.js 实现可交互的 3D 烟花秀:不是动效库堆砌,而是粒子系统+时间轴+GPU 渲染的协同工程

你点开一个index.html,页面黑底中央跳出倒计时数字,最后 3 秒开始加速闪烁,00:00 一到——不是预渲染视频,不是 CSS 动画拼接,而是上百个粒子从发射点沿抛物线升空、在指定高度炸裂成扇形光斑、每颗光斑再按衰减函数变色消散。这不是“前端炫技 demo”,而是真实运行在浏览器 GPU 上的物理模拟:每个粒子有独立位置、速度、加速度、生命周期、RGBα 值,且全部由 WebGL 渲染管线并行处理。countdown-master这个名字暴露了它的双重身份——既是跨年倒计时控制器,又是烟花粒子系统的调度中枢。它适合两类人:想把节日互动页从静态海报升级为沉浸式体验的前端工程师;以及正在学 Three.js 粒子系统但卡在“为什么粒子飞不起来”“为什么爆炸后不消失”的初学者。项目不依赖 Unity 或 Blender 导出,纯 JS + HTML + CSS 构建,所有逻辑集中在index.jsindex.html仅提供 canvas 容器和倒计时 DOM 节点。

2. Three.js 粒子系统设计原理与核心参数配置

2.1 为什么选 Three.js 而非 Canvas2D 或 CSS3D?

Three.js 提供了完整的 WebGL 封装层,其Points对象天然适配粒子系统需求:单个几何体(BufferGeometry)可承载数万顶点,每个顶点通过InstancedMeshPointsMaterial绑定独立属性(如颜色、大小、透明度)。对比 Canvas2D 的ctx.fillRect()每帧重绘数百粒子,性能差距达 5–8 倍;而 CSS3D 依赖 DOM 元素 transform,超过 50 个粒子即触发强制重排,内存占用陡增。countdown-masterinitScene()函数初始化的是WebGLRenderer,而非CanvasRenderer,这决定了它必须走 GPU 渲染路径。关键证据在index.js第 42 行:renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });——alpha: true启用透明背景,使烟花能叠加在任意网页底图上;antialias: true开启抗锯齿,避免粒子边缘出现阶梯状噪点。

2.2 烟花粒子的三层结构:发射器 → 爆炸体 → 残片

整个烟花效果被拆解为三个逻辑层级,对应 Three.js 中三个独立Points实例:

  • 发射器层(Launcher):负责生成初始粒子。每个发射器是一个THREE.Vector3,存储(x, y, z)发射坐标和(vx, vy, vz)初始速度。代码中通过createLauncher(x, y, z, vx, vy, vz)函数创建,vy恒为负值(向上发射),vx/vz添加随机扰动模拟风偏。
  • 爆炸体层(Explosion):当发射器粒子到达目标高度(y > 80)时,触发爆炸事件,销毁该发射器,并生成 120 个新粒子作为爆炸主体。这些粒子共享同一BufferGeometry,但通过setDynamic(true)启用运行时属性更新。
  • 残片层(Debris):爆炸体粒子在生命周期结束前,以 30% 概率分裂出 3–5 个更小的残片粒子,模拟火星飞溅效果。残片使用不同材质(PointsMaterialsize缩小至 0.3,opacity降至 0.6)。

提示:countdown-master中未使用THREE.GPUComputationRenderer,说明作者选择 CPU 计算粒子状态而非 GPU 并行计算。这意味着粒子总数建议控制在 5000 以内,否则主线程会因updateParticles()循环阻塞导致倒计时跳帧。

2.3 粒子状态更新的核心循环与时间戳校准

index.jsanimate()函数是主循环入口,但关键逻辑藏在updateParticles()中:

function updateParticles() { const now = performance.now(); const deltaTime = (now - lastTime) / 1000; // 转换为秒 lastTime = now; // 更新发射器粒子:应用重力加速度 for (let i = 0; i < launchers.length; i++) { const p = launchers[i]; p.velocity.y += GRAVITY * deltaTime; // 重力加速度 9.8 m/s² p.position.x += p.velocity.x * deltaTime; p.position.y += p.velocity.y * deltaTime; p.position.z += p.velocity.z * deltaTime; // 到达爆炸高度则触发爆炸 if (p.position.y > EXPLODE_HEIGHT && !p.exploded) { explode(p.position); p.exploded = true; } } // 更新爆炸体粒子:衰减生命周期 & 颜色 for (let i = 0; i < explosions.length; i++) { const p = explosions[i]; p.life -= deltaTime; if (p.life <= 0) { p.active = false; continue; } // 颜色随生命周期线性插值:从亮黄(1,1,0)到暗红(0.5,0,0) p.color.lerpColors(new THREE.Color(1, 1, 0), new THREE.Color(0.5, 0, 0), 1 - p.life / MAX_LIFE); } }

这段代码揭示了三个硬性约束:

  • deltaTime必须用performance.now()计算,而非requestAnimationFrame的时间参数——后者在页面后台时可能暂停,导致倒计时与烟花不同步;
  • GRAVITY值设为150(非 9.8),因为 Three.js 单位是像素而非米,需按比例缩放;
  • MAX_LIFE固定为1.2秒,意味着所有爆炸粒子必须在此时间内完成消散,否则残留粒子会堆积内存。
2.3.1 粒子材质参数表:决定视觉质感的关键开关
参数名默认值作用说明修改建议
size2.0粒子渲染尺寸(像素)爆炸体设为3.0,残片设为0.8,避免视觉混淆
sizeAttenuationtrue启用透视缩放:远处粒子自动缩小必须为true,否则 3D 空间感丧失
transparenttrue启用透明通道混合关键!否则粒子叠加处颜色过曝
blendingTHREE.AdditiveBlending加性混合:RGB 值相加,产生发光效果不可改为NormalBlending,否则烟花失去辉光
depthWritefalse关闭深度写入防止粒子相互遮挡,确保所有粒子可见

这些参数在createExplosionMaterial()函数中统一配置,直接修改material.size即可实时生效,无需重建几何体。

3. 倒计时与烟花触发的精确协同机制

3.1 倒计时状态机:从 idle → counting → exploding → finished

countdown-master的倒计时并非简单setInterval,而是基于Date对象构建的状态机。核心逻辑在startCountdown()函数:

function startCountdown() { const targetDate = new Date('2025-01-29T00:00:00'); // 春节零点 const now = new Date(); if (now >= targetDate) { state = 'finished'; triggerFireworks(); // 立即触发烟花 return; } state = 'counting'; countdownInterval = setInterval(() => { const now = new Date(); const diff = targetDate - now; if (diff <= 0) { clearInterval(countdownInterval); state = 'exploding'; triggerFireworks(); return; } // 计算天/时/分/秒 const days = Math.floor(diff / (1000 * 60 * 60 * 24)); const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((diff % (1000 * 60)) / 1000); // 更新 DOM document.getElementById('days').textContent = padZero(days); document.getElementById('hours').textContent = padZero(hours); document.getElementById('minutes').textContent = padZero(minutes); document.getElementById('seconds').textContent = padZero(seconds); // 最后 3 秒启用闪烁动画 if (diff < 3000) { document.body.style.animation = 'pulse 0.5s infinite'; } }, 100); // 100ms 刷新率,平衡精度与性能 }

这段代码的关键设计点:

  • targetDate硬编码为春节日期(2025-01-29),若需跨年使用,应改为new Date().getFullYear() + 1动态计算;
  • diff以毫秒为单位,避免Date对象的时区转换误差;
  • 100ms刷新间隔是权衡点:1000ms会导致倒计时跳秒,10ms则过度消耗 CPU。

3.2 烟花触发器:如何避免多点击导致粒子爆炸失控?

triggerFireworks()函数是防抖关键:

let fireworksTriggered = false; function triggerFireworks() { if (fireworksTriggered) return; // 防重复触发 fireworksTriggered = true; // 重置所有粒子池 launchers = []; explosions = []; debris = []; // 批量生成 8 个发射器,位置随机分布在屏幕底部 for (let i = 0; i < 8; i++) { const x = (Math.random() - 0.5) * 200; // -100 ~ +100 const z = (Math.random() - 0.5) * 200; // -100 ~ +100 const vy = -Math.random() * 120 - 80; // 初始向上速度 createLauncher(x, -100, z, 0, vy, 0); } // 3 秒后自动关闭触发锁,允许下一轮 setTimeout(() => { fireworksTriggered = false; }, 3000); }

这里采用“单次触发 + 时间锁”机制,而非事件监听器移除。fireworksTriggered标志位确保即使用户疯狂点击按钮,也只执行一次烟花生成。setTimeout的 3 秒窗口覆盖了最长烟花持续时间(爆炸体 1.2s + 残片 0.8s),防止粒子未清空就再次触发。

3.3 交互增强:键盘空格键与鼠标点击双触发支持

countdown-master隐藏了键盘支持,需在initEventListeners()中补全:

document.addEventListener('keydown', (e) => { if (e.code === 'Space' && state === 'counting') { e.preventDefault(); // 阻止页面滚动 triggerFireworks(); } }); document.addEventListener('click', (e) => { // 仅在倒计时区域点击有效 if (e.target.closest('#countdown') && state === 'counting') { triggerFireworks(); } });

此设计让项目具备无障碍访问能力:视障用户可通过键盘触发,触屏设备用户点击倒计时区域即可。注意e.preventDefault()必须添加,否则空格键会触发<body>滚动。

4. 性能调优与跨设备兼容性实战方案

4.1 粒子数量动态降级策略:根据设备性能自动调节

countdown-master默认生成 8 个发射器 × 120 爆炸粒子 = 960 粒子,但在低端 Android 设备上易掉帧。需加入性能探测:

function detectPerformance() { const fps = 60; const now = performance.now(); const frameTime = 1000 / fps; // 模拟 10 帧渲染耗时 let totalRenderTime = 0; for (let i = 0; i < 10; i++) { const start = performance.now(); // 执行一次粒子更新(不渲染) updateParticles(); totalRenderTime += performance.now() - start; } const avgFrameMs = totalRenderTime / 10; if (avgFrameMs > frameTime * 1.5) { // CPU 负载过高,降级为 4 发射器 LAUNCHER_COUNT = 4; } else if (avgFrameMs > frameTime * 1.2) { // 中等负载,保持 8 发射器 LAUNCHER_COUNT = 8; } else { // 高性能设备,提升至 12 发射器 LAUNCHER_COUNT = 12; } }

该函数应在initScene()后立即调用,LAUNCHER_COUNT作为全局常量影响triggerFireworks()中的循环次数。实测表明,在骁龙 660 设备上,LAUNCHER_COUNT=4可维持 55fps,而=8会跌至 32fps。

4.2 移动端适配:触摸事件与 DPR 修正

index.js中缺失移动端关键适配,需补充:

// 修正高 DPR 屏幕的 canvas 尺寸 function resizeRenderer() { const dpr = window.devicePixelRatio || 1; const width = window.innerWidth * dpr; const height = window.innerHeight * dpr; renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(dpr); // 告知 Three.js 使用高分辨率 camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); } window.addEventListener('resize', resizeRenderer); resizeRenderer(); // 初始化 // 触摸事件支持 let touchStartX = 0; document.addEventListener('touchstart', (e) => { touchStartX = e.touches[0].clientX; }, { passive: true }); document.addEventListener('touchend', (e) => { const touchEndX = e.changedTouches[0].clientX; if (Math.abs(touchEndX - touchStartX) < 30 && state === 'counting') { triggerFireworks(); // 水平滑动距离小于 30px 视为点击 } });

renderer.setPixelRatio(dpr)是移动端清晰显示的必要条件,否则粒子边缘模糊;touchend事件中判断滑动距离,避免误触。

4.3 内存泄漏防护:粒子对象的显式回收

Three.js 的Points对象不会自动垃圾回收,需手动清理:

function clearAllParticles() { // 清空几何体数据 if (launcherGeometry) { launcherGeometry.dispose(); } if (explosionGeometry) { explosionGeometry.dispose(); } // 清空材质 if (launcherMaterial) { launcherMaterial.dispose(); } if (explosionMaterial) { explosionMaterial.dispose(); } // 重置数组 launchers = []; explosions = []; debris = []; }

此函数应在triggerFireworks()开头调用,确保每次烟花秀前内存干净。dispose()方法释放 GPU 显存,避免 Chrome DevTools 的 Memory 面板中WebGLTexture持续增长。

5. 自定义烟花效果:修改爆炸模式与音效同步技巧

5.1 五种爆炸模式切换:球形/锥形/环形/螺旋/随机扩散

countdown-master默认使用球形爆炸(Math.random()生成三维方向向量),但可通过修改explode()函数中的粒子方向生成逻辑实现其他模式:

function explode(position) { const mode = 'spiral'; // 可选:'sphere', 'cone', 'ring', 'spiral', 'random' for (let i = 0; i < 120; i++) { const p = { position: position.clone(), velocity: new THREE.Vector3(), life: MAX_LIFE, color: new THREE.Color(1, 1, 0), active: true }; switch(mode) { case 'sphere': p.velocity.x = (Math.random() - 0.5) * 200; p.velocity.y = (Math.random() - 0.5) * 200; p.velocity.z = (Math.random() - 0.5) * 200; break; case 'spiral': const angle = (i / 120) * Math.PI * 4; const radius = 50 + Math.sin(angle * 2) * 20; p.velocity.x = Math.cos(angle) * radius; p.velocity.y = Math.sin(angle) * radius * 0.3; p.velocity.z = Math.sin(angle * 0.5) * radius; break; case 'ring': const ringAngle = (i / 120) * Math.PI * 2; p.velocity.x = Math.cos(ringAngle) * 100; p.velocity.y = 0; p.velocity.z = Math.sin(ringAngle) * 100; break; } explosions.push(p); } }

spiral模式生成阿基米德螺旋轨迹,视觉上呈现旋转升腾效果;ring模式形成水平光环,适合模拟“礼花弹”经典造型。修改mode变量即可实时切换,无需重启页面。

5.2 Web Audio API 同步音效:让爆炸声与粒子绽放帧精准对齐

countdown-master无音频,但可无缝集成:

let audioContext; let explosionBuffer; function initAudio() { audioContext = new (window.AudioContext || window.webkitAudioContext)(); // 预加载爆炸音效(base64 编码或 fetch 加载) fetch('explosion.mp3') .then(res => res.arrayBuffer()) .then(data => audioContext.decodeAudioData(data)) .then(buffer => explosionBuffer = buffer); } function playExplosionSound() { if (!explosionBuffer || !audioContext) return; const source = audioContext.createBufferSource(); source.buffer = explosionBuffer; source.connect(audioContext.destination); source.start(); // 此刻触发,与粒子生成完全同步 } // 在 explode() 函数末尾调用 function explode(position) { // ... 粒子生成逻辑 playExplosionSound(); }

关键点:source.start()无参数时即刻播放,与explode()函数执行时刻一致。若需延迟,可传入audioContext.currentTime + 0.1实现 100ms 延迟,匹配声音传播物理延迟。

5.2.1 音效资源嵌入方案:免 HTTP 请求的 base64 方式

将 MP3 文件转为 base64 字符串,直接写入 JS:

const explosionBase64 = "data:audio/mp3;base64,SUQzBAAAAAABEVRYWFgAAAAtAAADY29tbWVudABCaWdTb2Z0d2FyZQBUSVQyAAAAHQAAAzIuMDMuMzUAVFNTRQAAAA8AAANMYXZmNTcuODMuMTAwAAAAAAAAAAAAAAD/80BEAAAAA//80EX/AMTEf/xVVf/zQsRbAAADSAAAAABVVf/zQsSEf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAAABVVf/zQsRbAAADSAAAA......"; function playExplosionSound() { if (!audioContext) initAudio(); const source = audioContext.createBufferSource(); source.buffer = audioContext.decodeAudioData(atob(explosionBase64.split(',')[1])); source.connect(audioContext.destination); source.start(); }

此方案避免跨域请求,所有音效内联,适合离线部署场景。atob()解码 base64 字符串,decodeAudioData()异步解码为音频缓冲区。

注意:base64 字符串过长会增加 JS 文件体积,建议仅用于短音效(< 500KB)。长音效仍推荐fetch加载。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询