news 2026/7/5 8:35:36

Three.js 粒子烟花教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Three.js 粒子烟花教程

粒子烟花 ·Fire· ▶ 在线运行案例

  • 案例合集:三维可视化功能案例(threehub.cn)
  • 开源仓库github地址:https://github.com/z2586300277/three-cesium-examples
  • 400个案例代码:网盘链接

你将学到什么

  • ShaderMaterial 自定义着色器实现核心视觉效果
  • OrbitControls 相机轨道交互
  • THREE.Points 粒子点渲染
  • GSAP 时间轴与补间动画
  • BufferGeometry 自定义顶点/索引数据
  • requestAnimationFrame渲染循环与resize自适应

效果说明

本案例演示粒子烟花效果:基于 WebGL 实现「粒子烟花」可视化效果,附完整可运行源码;核心用到 ShaderMaterial、OrbitControls、THREE.Points。建议先打开文首在线案例查看动态画面,再对照下方源码逐步理解。

核心概念

  • Scene / Camera / WebGLRenderer构成最小渲染闭环;大场景可开logarithmicDepthBuffer缓解 Z-fighting。
  • ShaderMaterial通过uniforms+ 自定义 GLSL 控制逐像素/逐点效果;透明粒子常配合depthTest: false
  • OrbitControls提供轨道旋转/缩放;开启enableDamping后需在 animate 中controls.update()
  • THREE.Points将每个顶点渲染为可控大小的粒子;可用自定义 attribute(如u_index)驱动片元/顶点动画。

实现步骤

  • 搭建 Scene、PerspectiveCamera、WebGLRenderer,挂载 canvas 并处理resize
  • 定义 uniforms / onBeforeCompile 或 ShaderMaterial,编写 GLSL 与材质参数
  • 创建 OrbitControls(及 Raycaster 等交互控件,若源码包含)
  • 在定时器或 GSAP 时间轴中更新 uniform / 变换,驱动特效播放
  • requestAnimationFrame循环中更新状态并 render(Cesium 为viewer.render或自动渲染)
  • 代码要点

    import * as THREE from "three";

    import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import gsap from "gsap";

    const scene = new THREE.Scene();

    const sizes = { width: window.innerWidth, height: window.innerHeight, resolution: null, pixelRatio: Math.min(window.devicePixelRatio, 2), }; sizes.resolution = new THREE.Vector2( window.innerWidth * sizes.pixelRatio, window.innerHeight * sizes.pixelRatio );

    const textureLoader = new THREE.TextureLoader();

    const textures = [ textureLoader.load(FILE_HOST + "threeExamples/particle/particleFire/1.png"), textureLoader.load(FILE_HOST + "threeExamples/particle/particleFire/10.png"), textureLoader.load(FILE_HOST + "threeExamples/particle/particleFire/3.png"), textureLoader.load(FILE_HOST + "threeExamples/particle/particleFire/4.png"), textureLoader.load(FILE_HOST + "threeExamples/particle/particleFire/5.png"), textureLoader.load(FILE_HOST + "threeExamples/particle/particleFire/6.png"), textureLoader.load(FILE_HOST + "threeExamples/particle/particleFire/7.png"), textureLoader.load(FILE_HOST + "threeExamples/particle/particleFire/8.png"), ];

    /**

    • * @param {粒子数目} count
    • @param {烟花位置} position
    • @param {烟花粒子大小} size
    • @param {纹理} texture
    • @param {烟花半径} radius
    • @param {颜色}color
    */ const createFireWork = async ( count, position, size, texture, radius = 1, color ) => { if (!texture && texture instanceof THREE.Texture) return; // 反转纹理 texture.flipY = false; const positionsArray = new Float32Array(count * 3);

    // 粒子的随机大小 const sizeArray = new Float32Array(count); // 粒子的随机存在寿命 const lifeArray = new Float32Array(count); for (let index = 0; index < count; index++) { const spherical = new THREE.Spherical( radius(0.75 + (Math.random() - 0.5)0.25), Math.random() * Math.PI, Math.random()Math.PI2 ); const position = new THREE.Vector3(); position.setFromSpherical(spherical);

    positionsArray[index * 3] = position.x; positionsArray[index * 3 + 1] = position.y; positionsArray[index * 3 + 2] = position.z;

    sizeArray[index] = Math.random(); // 粒子的寿命只能够在原有的基础上的更短, //这样烟花粒子就消失的更快,会在vs基于原有的寿命乘上这个值 lifeArray[index] = Math.random() + 1; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute( "position", new THREE.Float32BufferAttribute(positionsArray, 3) ); geometry.setAttribute( "aSize", new THREE.Float32BufferAttribute(sizeArray, 1) ); geometry.setAttribute( "aLife", new THREE.Float32BufferAttribute(lifeArray, 1) ); const material = new THREE.ShaderMaterial({ fragmentShader:precision mediump float; uniform sampler2D uTexture; uniform vec3 uColor; varying vec2 vUv; uniform float uTime; varying vec3 vPosition; varying vec3 vNormal; void main(){ // 注意开启材质透明 float textureAlpha=texture(uTexture,gl_PointCoord).r; gl_FragColor=vec4(uColor, textureAlpha); // 引入three.js的内置shader代码。开启toneMapping和colorSpace #include #include }, vertexShader:#include precision mediump float; attribute float aSize; attribute float aLife; uniform float uTime; uniform float uSize; uniform vec2 uResolution; uniform float uProgress; varying vec3 vPosition; varying vec3 vNormal; float linearFunction (float x,float x1,float y1,float x2,float y2) { return x((y2-y1)/(x2-x1))+(y2-((y2-y1)/(x2-x1))x2); } void main(){ /**

    • Position
    */ vec3 newPosition=position; newPosition=newPosition; float vProgress=uProgress; vProgress*=aLife; //Explding // float explodingProgress=uProgress*((1.0-0)/(0.1-0)); float explodingProgress=vProgress*10.0; explodingProgress=clamp(explodingProgress,0.0 ,1.0 ); explodingProgress=1.0-pow(1.0-explodingProgress,3.0); newPosition*=explodingProgress; // Falling // fallingProgress float fallingProgress=linearFunction(vProgress,0.1,0.0,1.0,1.0); fallingProgress=clamp(fallingProgress,0.0 ,1.0 ); fallingProgress=(1.0-pow(1.0-fallingProgress,3.0))*0.2; newPosition.y-=fallingProgress; //scalling float sizeOpenProgress=linearFunction(vProgress,0.0,0.0,0.125,1.0); float sizecCloseProgress=linearFunction(vProgress,0.125,1.0,1.0,0.0); float scallingProcess =min(sizecCloseProgress,sizecCloseProgress ) ; scallingProcess=clamp(scallingProcess,0.0 ,1.0 ); //Twinkling float twinkProgreess=linearFunction(vProgress,0.2,0.0,0.8,1.0); twinkProgreess=clamp(twinkProgreess,0.0 ,1.0 ); float twinkSize=sin(vProgress30.0)0.5+0.5; twinkSize=1.0-twinkProgreess*twinkSize; vec4 modelPosition=modelMatrix*vec4(newPosition,1.); vec4 viewPosition=viewMatrix*modelPosition; vec4 projectedPosition=projectionMatrix*viewPosition; gl_Position=projectedPosition; gl_PointSize=uSize*uResolution.y; gl_PointSize*=aSize; gl_PointSize*=scallingProcess; gl_PointSize*=twinkSize; // 实现粒子的透视效果,viewPosition为是模型视图变化后的posotion gl_PointSize*=(1.0/-viewPosition.z); if(gl_PointSize<1.0) gl_Position=vec4(9999.9); }, uniforms: { uSize: new THREE.Uniform(size), // 屏幕分辨率 uResolution: new THREE.Uniform(sizes.resolution), uTexture: new THREE.Uniform(texture), uColor: new THREE.Uniform(color), uProgress: new THREE.Uniform(0), }, transparent: true, // 关闭粒子深度测试 depthTest: false, // 开启混合 blending: THREE.AdditiveBlending, }); const fireWork = new THREE.Points(geometry, material); fireWork.position.copy(position); scene.add(fireWork);

    //Destory const destory = () => { scene.remove(fireWork); geometry.dispose(); material.dispose(); };

    // Animate gsap.to(material.uniforms.uProgress, { value: 1, duration: 3, ease: "linear", onComplete: destory, }); };

    const radomCreateFireWork = () => { const count = Math.round(400 + Math.random() * 1000); const position = new THREE.Vector3( (Math.random() - 0.5) * 2, Math.random(), (Math.random() - 0.5) * 2 ); const size = 0.1 + Math.random() * 0.1; const texture = textures[Math.floor(Math.random() * textures.length)]; const radius = 1 + Math.random(); const color = new THREE.Color(); color.setHSL(Math.random(), 1, 0.7); createFireWork(count, position, size, texture, radius, color); };

    setInterval(radomCreateFireWork, 600);

    const camera = new THREE.PerspectiveCamera( 75, sizes.width / sizes.height, 0.1, 100 ); camera.position.set(0, 0, 2); scene.add(camera);

    const renderer = new THREE.WebGLRenderer({ antialias: true, }); renderer.setSize(sizes.width, sizes.height); renderer.setPixelRatio(sizes.pixelRatio); document.getElementById("box").appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true;

    const tick = () => {

    controls.update()

    renderer.render(scene, camera)

    requestAnimationFrame(tick)

    };

    tick();

    完整源码:GitHub

    小结

    • 本文提供粒子烟花完整 Three.js 源码与在线 Demo,建议先运行案例再改 uniform/参数做二次实验
    • 更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/5 8:34:27

想了解乡墅赋能培训?这家超靠谱的乡墅赋能培训机构推荐给你!

在乡村振兴的大背景下&#xff0c;乡墅市场迎来了前所未有的发展机遇。然而&#xff0c;许多从业者在实际运营中面临着诸多挑战&#xff0c;乡墅赋能培训应运而生。湖北乡墅研究中心便是一家在该领域表现出色的机构。乡墅行业现状与痛点行业报告显示&#xff0c;当前乡墅行业发…

作者头像 李华
网站建设 2026/7/5 8:34:14

Python A2A实战:为AI Agent创建专属邮箱与多智能体协作网络

1. 先搞清楚 A2A 和 AI Agent 邮箱到底要解决什么问题如果你最近在关注 AI Agent 开发&#xff0c;大概率会看到“A2A时代”和“为 AI Agent 申请专属邮箱”这类说法。这背后其实是一个很实际的问题&#xff1a;当多个 AI Agent 需要像人一样协作时&#xff0c;它们之间如何可靠…

作者头像 李华
网站建设 2026/7/5 8:33:32

性能测试结果深度解读:从指标分析到瓶颈定位实战指南

1. 性能测试结果解读&#xff1a;从数据迷雾到系统真相刚做完一轮性能压测&#xff0c;看着JMeter或LoadRunner生成的那一堆花花绿绿的图表和密密麻麻的数字&#xff0c;是不是感觉头都大了&#xff1f;响应时间、TPS、错误率、CPU使用率……每个指标好像都在说话&#xff0c;但…

作者头像 李华
网站建设 2026/7/5 8:31:29

静态文件服务器XSS攻击:文件上传场景下的安全盲区与防御实践

1. 项目概述&#xff1a;一个被忽视的“安全盲区”“静态文件服务器”和“XSS攻击”&#xff0c;这两个词放在一起&#xff0c;很多开发者第一反应可能是&#xff1a;“这俩有关系吗&#xff1f;” 在很多人的认知里&#xff0c;静态文件服务器&#xff0c;比如Nginx、Apache直…

作者头像 李华
网站建设 2026/7/5 8:29:47

直方图均衡化 5 大应用场景实战:医学影像、遥感与低光照图像增强

直方图均衡化在医学影像与遥感图像中的5大高阶应用当我们需要从一张X光片中识别细微的骨折线&#xff0c;或是从卫星图像中分辨不同作物类型时&#xff0c;图像对比度往往成为关键瓶颈。直方图均衡化作为经典的图像增强技术&#xff0c;通过重新分配像素强度值&#xff0c;能够…

作者头像 李华
网站建设 2026/7/5 8:28:55

Windows系统实战部署PGP加密:从GnuPG安装到邮件文件安全应用

1. 项目概述&#xff1a;为什么今天还要用PGP&#xff1f;如果你在互联网上稍微有点年头&#xff0c;应该听过PGP这个名字。它就像一个数字世界的“瑞士军刀”&#xff0c;专门用来解决一个最古老也最核心的问题&#xff1a;如何确保只有你和你想沟通的人&#xff0c;才能看到信…

作者头像 李华