news 2026/7/4 12:48:12

Three.js 水流粒子教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Three.js 水流粒子教程

水流粒子 ·Water Leakage· ▶ 在线运行案例

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

你将学到什么

  • ShaderMaterial 自定义着色器实现核心视觉效果
  • OrbitControls 相机轨道交互
  • THREE.Points 粒子点渲染
  • 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 等交互控件,若源码包含)
  • requestAnimationFrame循环中更新状态并 render(Cesium 为viewer.render或自动渲染)
  • 代码要点

    import * as THREE from 'three'

    import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'

    const box = document.getElementById('box') const scene = new THREE.Scene() const camera = new THREE.PerspectiveCamera(75, box.clientWidth / box.clientHeight, 0.0000001, 100000) // 调整相机的近裁剪面为更小的值,让近距离的粒子可见 camera.position.set(10, 10, 5) const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, logarithmicDepthBuffer: true }) renderer.setSize(box.clientWidth, box.clientHeight) box.appendChild(renderer.domElement) new OrbitControls(camera, renderer.domElement) scene.add(new THREE.AxesHelper(10))

    const mesh = createMesh() scene.add(mesh)

    function animate() { mesh.render() requestAnimationFrame(animate); renderer.render(scene, camera); } animate()

    window.onresize = () => { renderer.setSize(box.clientWidth, box.clientHeight) camera.aspect = box.clientWidth / box.clientHeight camera.updateProjectionMatrix() }

    /方法/ function createMesh() {

    const params = { maxParticles: 3000, spawnRate: 12, gravity: 10, minSize: 0.5, maxSize: 1, minStrength: 1, maxStrength: 4, spread: 0.6, burstProbability: 0.7, burstMultiplier: 3, color: "#9da7af", blendingMode: "Additive", rainLength: 0.6, opacity: 0.6, collisionY: 0, // 改名为collisionY,表示碰撞高度,而不是地面 enableSplash: true, splashParticles: 6, splashSize: 0.3, splashSpeed: 3, splashLifeTime: 0.6 }

    // 设置UI const gui = new GUI(); const particleFolder = gui.addFolder('粒子系统'); particleFolder.add(params, 'maxParticles').name('最大粒子数').onChange(value => { scene.remove(emitter.points); emitter = new ParticleSystem(value); scene.add(emitter.points); }); particleFolder.add(params, 'spawnRate').name('生成速率');

    const particlePhysicsFolder = gui.addFolder('物理属性'); particlePhysicsFolder.add(params, 'gravity').name('重力'); particlePhysicsFolder.add(params, 'minStrength').name('最小喷射强度'); particlePhysicsFolder.add(params, 'maxStrength').name('最大喷射强度'); particlePhysicsFolder.add(params, 'spread').name('发散程度').step(0.01).max(1).min(0)

    const particleVisualFolder = gui.addFolder('视觉属性'); particleVisualFolder.add(params, 'minSize').name('最小粒子尺寸'); particleVisualFolder.add(params, 'maxSize').name('最大粒子尺寸'); particleVisualFolder.addColor(params, 'color').name('粒子颜色').onChange(value => { emitter.material.uniforms.color.value.set(value); splashSystem.material.uniforms.color.value.set(value); // 同步更新水花颜色 }); particleVisualFolder.add(params, 'burstProbability').name('突发概率'); particleVisualFolder.add(params, 'burstMultiplier').name('突发倍数'); particleVisualFolder.add(params, 'blendingMode', ['Additive', 'Normal']).name('混合模式').onChange(value => { emitter.material.blending = value === 'Additive' ? THREE.AdditiveBlending : THREE.NormalBlending; emitter.material.needsUpdate = true; }); particleVisualFolder.add(params, 'rainLength', 0.1, 1.0).name('雨滴长度').onChange(value => { emitter.material.uniforms.rainLength.value = value; }); particleVisualFolder.add(params, 'opacity', 0.1, 1.0).step(0.1).name('整体透明度').onChange(value => { emitter.material.uniforms.globalOpacity.value = value; splashSystem.material.uniforms.globalOpacity.value = value; // 同步更新水花透明度 });

    const splashFolder = gui.addFolder('水花效果'); splashFolder.add(params, 'enableSplash').name('启用水花效果'); splashFolder.add(params, 'splashParticles', 1, 15).step(1).name('水花粒子数'); splashFolder.add(params, 'splashSize', 0.1, 1.0).name('水花大小'); splashFolder.add(params, 'splashSpeed', 1, 10).name('水花飞溅速度'); splashFolder.add(params, 'splashLifeTime', 0.1, 2.0).name('水花生命时间'); splashFolder.add(params, 'collisionY').name('碰撞高度'); // 修改名称 splashFolder.open();

    particleFolder.open(); particlePhysicsFolder.open(); particleVisualFolder.open();

    class SplashParticle { constructor(position) { this.position = position.clone(); const angle = Math.random()Math.PI2; const speed = Math.random() * params.splashSpeed; this.velocity = new THREE.Vector3( Math.cos(angle) * speed, Math.random()speed0.8 + speed * 0.5, Math.sin(angle) * speed ); this.life = 0; this.maxLife = params.splashLifeTime(0.7 + Math.random()0.6); this.size = params.splashSize(0.5 + Math.random()0.5); }

    update(delta) { this.velocity.y -= params.gravitydelta0.8; this.position.addScaledVector(this.velocity, delta); this.life += delta; if (this.position.y < params.collisionY) { // 更新属性名 this.position.y = params.collisionY; this.velocity.y = 0; } return this.life <= this.maxLife; } }

    class SplashSystem { constructor(maxCount = 1000) { this.maxCount = maxCount; this.particles = []; this.geometry = new THREE.BufferGeometry(); this.positions = new Float32Array(maxCount * 3); this.sizes = new Float32Array(maxCount); this.opacities = new Float32Array(maxCount); this.geometry.setAttribute('position', new THREE.BufferAttribute(this.positions, 3)); this.geometry.setAttribute('size', new THREE.BufferAttribute(this.sizes, 1)); this.geometry.setAttribute('opacity', new THREE.BufferAttribute(this.opacities, 1)); this.geometry.setDrawRange(0, 0); this.material = new THREE.ShaderMaterial({ uniforms: { color: { value: new THREE.Color(params.color) }, globalOpacity: { value: params.opacity } }, vertexShader:attribute float size; attribute float opacity; varying float vOpacity; void main() { vOpacity = opacity; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); // 改进粒子大小计算,防止在近距离时粒子消失 float distance = max(length(mvPosition.xyz), 0.1); // 防止除以接近0的值 float scale = 300.0 / distance; // 限制最大缩放比例,防止过近时粒子过大 scale = min(scale, 50.0); gl_PointSize = size * scale; gl_Position = projectionMatrix * mvPosition; }, fragmentShader:uniform vec3 color; uniform float globalOpacity; varying float vOpacity; void main() { vec2 center = vec2(0.5, 0.5); float dist = distance(gl_PointCoord, center); float alpha = smoothstep(0.5, 0.2, dist)vOpacityglobalOpacity; if (alpha < 0.01) discard; gl_FragColor = vec4(color, alpha); }, blending: THREE.AdditiveBlending, transparent: true }); this.points = new THREE.Points(this.geometry, this.material); scene.add(this.points); }

    addSplash(position) { if (!params.enableSplash) return; for (let i = 0; i < params.splashParticles; i++) { if (this.particles.length < this.maxCount) { this.particles.push(new SplashParticle(position)); } } }

    update(delta) { let alive = 0; let aliveParticles = []; for (let i = 0; i < this.particles.length; i++) { const p = this.particles[i]; if (p.update(delta)) { this.positions[alive * 3] = p.position.x; this.positions[alive * 3 + 1] = p.position.y; this.positions[alive * 3 + 2] = p.position.z; this.sizes[alive] = p.size; this.opacities[alive] = 1.0 - (p.life / p.maxLife); alive++; aliveParticles.push(p); } } this.particles = aliveParticles; this.geometry.setDrawRange(0, alive); this.geometry.attributes.position.needsUpdate = true; this.geometry.attributes.size.needsUpdate = true; this.geometry.attributes.opacity.needsUpdate = true; } }

    const splashSystem = new SplashSystem(); splashSystem.points.frustumCulled = false // 禁用视锥体剔除,以确保水花在相机外也能渲染

    class Particle { constructor() { this.position = new THREE.Vector3(0, 5, 0); const angle = Math.random()Math.PI2; const strength = Math.random() * (params.maxStrength - params.minStrength) + params.minStrength; const spread = Math.random() * params.spread; this.velocity = new THREE.Vector3( Math.cos(angle)spreadstrength, -Math.random() * strength - 2, Math.sin(angle)spreadstrength ); this.life = 0; this.maxLife = 1 + Math.random() * 0.5; this.size = this.initialSize = Math.random() * (params.maxSize - params.minSize) + params.minSize; } update(delta) { this.velocity.y -= params.gravitydelta0.5; this.position.addScaledVector(this.velocity, delta); this.life += delta; const lifeRatio = this.life / this.maxLife; this.size = this.initialSize(1 - lifeRatio0.5); if (this.position.y <= params.collisionY && this.velocity.y < 0) { // 更新属性名 splashSystem.addSplash(new THREE.Vector3(this.position.x, params.collisionY, this.position.z)); // 更新属性名 this.life = this.maxLife + 1; } return this.life <= this.maxLife; } }

    class ParticleSystem { constructor(maxCount = params.maxParticles) { this.maxCount = maxCount; this.particles = []; this.geometry = new THREE.BufferGeometry(); this.positions = new Float32Array(this.maxCount * 3); this.sizes = new Float32Array(this.maxCount); this.opacities = new Float32Array(this.maxCount); this.geometry.setAttribute('position', new THREE.BufferAttribute(this.positions, 3)); this.geometry.setAttribute('size', new THREE.BufferAttribute(this.sizes, 1)); this.geometry.setAttribute('opacity', new THREE.BufferAttribute(this.opacities, 1)); this.geometry.setDrawRange(0, 0); this.material = new THREE.ShaderMaterial({ uniforms: { color: { value: new THREE.Color(params.color) }, rainLength: { value: params.rainLength }, globalOpacity: { value: params.opacity } }, vertexShader:attribute float size; attribute float opacity; varying float vOpacity; void main() { vOpacity = opacity; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); // 改进粒子大小计算,防止在近距离时粒子消失 float distance = max(length(mvPosition.xyz), 0.1); // 防止除以接近0的值 float scale = 300.0 / distance; // 限制最大缩放比例,防止过近时粒子过大 scale = min(scale, 50.0); gl_PointSize = size * scale; gl_Position = projectionMatrix * mvPosition; }, fragmentShader:uniform vec3 color; uniform float rainLength; uniform float globalOpacity; varying float vOpacity; void main() { vec2 center = vec2(0.5, 0.5); vec2 uv = gl_PointCoord - center; float width = 0.05; float y_offset = (gl_PointCoord.y - 0.5) * 2.0; float shape = step(abs(uv.x), width * (1.0 - pow(y_offset, 2.0))); shape= step(gl_PointCoord.y, 0.5 + rainLength / 2.0)step(0.5 - rainLength / 2.0, gl_PointCoord.y); shape= 1.0 - (0.5 - gl_PointCoord.y)0.5; float alpha = shapevOpacityglobalOpacity; if (alpha < 0.01) discard; gl_FragColor = vec4(color, alpha); }, blending: params.blendingMode === 'Additive' ? THREE.AdditiveBlending : THREE.NormalBlending, depthTest: false, side: THREE.DoubleSide, transparent: true }); this.points = new THREE.Points(this.geometry, this.material); } spawn(count = params.spawnRate) { const burst = Math.random() > params.burstProbability ? params.burstMultiplier : 1; for (let i = 0, n = count * burst; i < n && this.particles.length < this.maxCount; i++) { this.particles.push(new Particle()); } } update(delta) { let alive = 0; let aliveParticles = []; for (let i = 0; i < this.particles.length; i++) { const p = this.particles[i]; if (p.update(delta)) { this.positions[alive * 3] = p.position.x; this.positions[alive * 3 + 1] = p.position.y; this.positions[alive * 3 + 2] = p.position.z; this.sizes[alive] = p.size; this.opacities[alive] = 1.0 - (p.life / p.maxLife); alive++; aliveParticles.push(p); } } this.particles = aliveParticles; this.geometry.setDrawRange(0, alive); this.geometry.attributes.position.needsUpdate = true; this.geometry.attributes.size.needsUpdate = true; this.geometry.attributes.opacity.needsUpdate = true; this.spawn(params.spawnRate); } }

    let emitter = new ParticleSystem(params.maxParticles) emitter.points.frustumCulled = false // 禁用视锥体剔除,以确保粒子在相机外也能渲染 // scene.add(emitter.points) const mesh = emitter.points const clock = new THREE.Clock() mesh.render = () => { const delta = clock.getDelta();

    // 确保水花系统始终使用最新的颜色和透明度值 splashSystem.material.uniforms.color.value.copy(emitter.material.uniforms.color.value); splashSystem.material.uniforms.globalOpacity.value = emitter.material.uniforms.globalOpacity.value; emitter.update(delta); splashSystem.update(delta); }

    return mesh

    }

    完整源码:GitHub

    小结

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

企业AI落地成本与ROI量化实战指南

1. 项目概述&#xff1a;这不是一场技术秀&#xff0c;而是一场财务与组织的双重压力测试“AI在企业中的真实成本与收益困境”——这个标题一出来&#xff0c;我就知道它戳中了太多老板、CIO和业务负责人的神经。过去两年&#xff0c;我帮27家企业做过AI落地可行性评估&#xf…

作者头像 李华
网站建设 2026/7/4 12:44:55

Windows隐私保护实战:从系统设置到PowerShell,全面掌控你的数据

你有没有过这样的感觉&#xff1a;新装的 Windows 系统&#xff0c;用着用着就“不对劲”了。开始菜单里冒出没安装过的应用推荐&#xff0c;搜索框里出现你最近浏览过的网页&#xff0c;甚至有时候&#xff0c;系统会自作主张地帮你“优化”一些设置。你隐约觉得&#xff0c;这…

作者头像 李华
网站建设 2026/7/4 12:44:33

速来!泛站群目录实操

以下是合规导向的实操技巧&#xff0c;完全避开违规操作&#xff0c;统筹功率与站点安全性&#xff1a;‌资源配备技巧‌优先选用洁净无违规记载的老域名&#xff0c;分配独立IP站点分组安排&#xff0c;防止多站同IP被批量牵连&#xff0c;用RAP东西的批量配备功用&#xff0c…

作者头像 李华
网站建设 2026/7/4 12:42:35

本科论文写作利器:AI工具全流程解决方案

1. 本科论文写作的痛点与AI工具价值 写本科论文是每个大学生都要经历的"成人礼"&#xff0c;但现实中90%的学生都会遇到这些典型问题&#xff1a;文献综述找不到方向、数据分析耗时费力、格式调整反复折腾、查重降重令人崩溃。更可怕的是&#xff0c;很多同学连基本的…

作者头像 李华
网站建设 2026/7/4 12:40:42

工业4-20mA电流环技术与DAC161S997单芯片方案解析

1. 工业4-20mA电流环技术背景解析在工业自动化领域&#xff0c;4-20mA电流环传输技术已有超过60年的应用历史。这种看似简单的模拟信号传输方式之所以能经久不衰&#xff0c;关键在于其独特的物理特性&#xff1a;电流信号在长距离传输时不受线路电阻影响&#xff0c;抗电磁干扰…

作者头像 李华