Three.js 高性能天气效果实现:下雨与下雪的 GPU 粒子系统

本文介绍如何使用 Three.js 的 InstancedMesh + 自定义 Shader 实现高性能的下雨和下雪效果,支持数万级粒子、无限循环、广告牌效果等特性。

效果预览

实现的天气效果具有以下特性:

  • 高性能 - 基于 GPU Instancing,支持 30000+ 雪花 / 50000+ 雨滴
  • 🔄 无限循环 - 粒子围绕相机循环,无视觉边界
  • 📷 广告牌效果 - 粒子始终面向相机
  • 🎨 可配置 - 支持数量、速度、大小、颜色等参数动态调节

核心技术选型

为什么选择 InstancedMesh?

传统粒子系统(Points)的问题:

  • 粒子只能是正方形点
  • 无法实现复杂形状(如拉长的雨滴)
  • 难以实现自定义着色

InstancedMesh 优势

  • 单次 Draw Call 渲染数万实例
  • 每个实例可以有独立的位置、缩放、旋转
  • 支持完全自定义的 Shader
javascript 复制代码
// 创建 InstancedMesh
const geometry = new THREE.PlaneGeometry(0.2, 0.2);
const material = new THREE.ShaderMaterial({ /* 自定义 Shader */ });
const mesh = new THREE.InstancedMesh(geometry, material, MAX_COUNT);

下雪效果实现 ❄️

1. 实例属性设计

每个雪花需要独立的属性:

javascript 复制代码
// 为每个雪花实例分配随机属性
const aOffset = new Float32Array(MAX_COUNT * 3);  // 初始位置偏移
const aSpeed = new Float32Array(MAX_COUNT);       // 下落速度
const aSwayFreq = new Float32Array(MAX_COUNT);    // 摇摆频率
const aSwayAmp = new Float32Array(MAX_COUNT);     // 摇摆幅度
const aScale = new Float32Array(MAX_COUNT);       // 大小缩放

for (let i = 0; i < MAX_COUNT; i++) {
  // 随机分布在 100x50x100 的空间内
  aOffset[i * 3] = (Math.random() - 0.5) * 100;     // X
  aOffset[i * 3 + 1] = (Math.random() - 0.5) * 50;  // Y
  aOffset[i * 3 + 2] = (Math.random() - 0.5) * 100; // Z
  
  aSpeed[i] = 2.0 + Math.random() * 3.0;
  aSwayFreq[i] = 1.0 + Math.random() * 2.0;
  aSwayAmp[i] = 0.5 + Math.random() * 1.0;
  aScale[i] = 0.5 + Math.random() * 1.0;
}

// 绑定为实例属性
geometry.setAttribute('aOffset', new THREE.InstancedBufferAttribute(aOffset, 3));
geometry.setAttribute('aSpeed', new THREE.InstancedBufferAttribute(aSpeed, 1));
// ...

2. 顶点着色器:无限循环

核心逻辑在顶点着色器中实现:

glsl 复制代码
uniform float uTime;
uniform float uHeight;        // 垂直范围
uniform float uRange;         // 水平范围
uniform vec3 uCameraPosition; // 相机位置
uniform float uSizeScale;
uniform float uSpeedScale;

attribute float aSpeed;
attribute float aSwayFreq;
attribute float aSwayAmp;
attribute vec3 aOffset;
attribute float aScale;

void main() {
  vec3 pos = aOffset;
  
  // 1. 动态下落(Y轴)
  float timeOffsetY = uTime * aSpeed * uSpeedScale;
  
  // 2. 无限循环:使用 mod 让粒子围绕相机循环
  pos.x = mod(aOffset.x - uCameraPosition.x, uRange) - uRange * 0.5 + uCameraPosition.x;
  pos.z = mod(aOffset.z - uCameraPosition.z, uRange) - uRange * 0.5 + uCameraPosition.z;
  pos.y = mod(aOffset.y - timeOffsetY - uCameraPosition.y, uHeight) - uHeight * 0.5 + uCameraPosition.y;

  // 3. 水平摇摆(模拟风吹雪花飘动)
  pos.x += sin(uTime * aSwayFreq + aOffset.y) * aSwayAmp;
  pos.z += cos(uTime * aSwayFreq + aOffset.x) * aSwayAmp;

  // 4. 广告牌效果:让平面始终面向相机
  vec4 mvPosition = viewMatrix * modelMatrix * vec4(pos, 1.0);
  mvPosition.xyz += position * aScale * uSizeScale;  // position 是平面的局部坐标

  gl_Position = projectionMatrix * mvPosition;
}

关键技巧解读

  1. mod 取模运算 - 让粒子在固定范围内循环,超出边界自动回到另一侧
  2. 相机位置跟随 - 循环范围始终以相机为中心,玩家移动时雪花跟随
  3. viewMatrix 应用 - 直接在视图空间中偏移顶点,实现广告牌效果

3. 片元着色器:圆形雪花 + 边缘渐隐

glsl 复制代码
uniform vec3 uColor;
uniform float uOpacity;
varying vec2 vUv;
varying float vAlpha;

void main() {
  // 计算到中心的距离,生成圆形
  float dist = distance(vUv, vec2(0.5));
  float alpha = smoothstep(0.5, 0.3, dist);

  if (alpha < 0.01) discard;

  // 应用边缘渐隐和全局透明度
  gl_FragColor = vec4(uColor, alpha * vAlpha * uOpacity);
}

边缘渐隐在顶点着色器中计算:

glsl 复制代码
// 距离相机越远,alpha 越低
float fadeLimit = uRange * 0.45;
vAlpha = smoothstep(fadeLimit, fadeLimit * 0.8, abs(pos.x - uCameraPosition.x));
vAlpha *= smoothstep(fadeLimit, fadeLimit * 0.8, abs(pos.z - uCameraPosition.z));

下雨效果实现 🌧️

下雨效果在雪的基础上增加了折射效果,让雨滴更真实。

1. 雨滴形状

雨滴是细长的矩形,通过缩放实现:

javascript 复制代码
dummy.scale.set(0.02, THREE.MathUtils.randFloat(0.4, 0.8), 0.02);

2. 背景折射(FBO 技术)

要实现雨滴的透明折射效果,需要:

  1. 先渲染背景到 RenderTarget(FBO)
  2. 雨滴采样 FBO 并偏移 UV 模拟折射
javascript 复制代码
// 创建低分辨率 FBO(性能优化)
this.bgFBO = new THREE.WebGLRenderTarget(
  canvas.width * 0.1,  // 10% 分辨率
  canvas.height * 0.1
);

// 渲染循环中
preRender() {
  // 1. 隐藏雨滴
  this.instancedMesh.visible = false;
  
  // 2. 渲染场景到 FBO
  this.renderer.setRenderTarget(this.bgFBO);
  this.renderer.render(this.scene, this.camera);
  this.renderer.setRenderTarget(null);
  
  // 3. 恢复雨滴,并传入 FBO 纹理
  this.instancedMesh.visible = true;
  this.rainMaterial.uniforms.uBgRt.value = this.bgFBO.texture;
}

3. 雨滴 Shader 中的折射计算

glsl 复制代码
// 顶点着色器:计算屏幕空间坐标
vec2 screenspace(mat4 proj, mat4 mv, vec3 pos) {
  vec4 temp = proj * mv * vec4(pos, 1.0);
  temp.xyz /= temp.w;
  temp.xy = 0.5 + temp.xy * 0.5;  // [-1,1] -> [0,1]
  return temp.xy;
}

varying vec2 vScreenspace;  // 传递给片元着色器
vScreenspace = screenspace(projectionMatrix, viewMatrix, finalPos);
glsl 复制代码
// 片元着色器:采样背景并偏移
uniform sampler2D uBgRt;
uniform float uRefraction;

void main() {
  // 计算雨滴法线(模拟圆柱形水滴)
  vec3 normal = vec3((vUv.x - 0.5) * 2.0, 0.0, 0.5);
  normal = normalize(normal);
  
  // 根据法线偏移 UV,模拟折射
  vec2 bgUv = vScreenspace + normal.xy * uRefraction;
  vec4 bgColor = texture2D(uBgRt, bgUv);
  
  // 添加高光和蓝色调
  float brightness = 0.8 * pow(max(0.0, normal.z), 4.0);
  vec3 col = bgColor.rgb + vec3(brightness);
  col += vec3(0.0, 0.05, 0.1) * alpha;  // 蓝色调
  
  gl_FragColor = vec4(col, alpha);
}

性能优化要点

1. InstancedMesh 复用

预分配最大数量的实例,通过 mesh.count 控制实际渲染数量:

javascript 复制代码
const mesh = new THREE.InstancedMesh(geometry, material, MAX_COUNT);
mesh.count = currentCount;  // 动态调整,无需重建

2. 低分辨率 FBO

雨效折射只需要模糊的背景信息,使用 10% 分辨率的 FBO 大幅降低性能开销。

3. 禁用视锥剔除

粒子系统的包围盒难以准确计算,直接禁用避免闪烁:

javascript 复制代码
mesh.frustumCulled = false;

4. GPU 端计算

所有位置更新、循环判断都在 Shader 中完成,CPU 只传递时间和相机位置。


封装为可复用模块

最终将天气效果封装为独立的管理器类:

javascript 复制代码
class SnowManager {
  constructor(scene, camera) {
    this.scene = scene;
    this.camera = camera;
    this._init();
  }
  
  setEnabled(enabled, config) { /* ... */ }
  updateConfig(config) { /* ... */ }
  update(time) {
    this.material.uniforms.uTime.value = time * 0.001;
    this.material.uniforms.uCameraPosition.value.copy(this.camera.position);
  }
  dispose() { /* 清理资源 */ }
}

使用方式:

javascript 复制代码
const snowManager = new SnowManager(scene, camera);

// 开启下雪
snowManager.setEnabled(true, { count: 10000, speed: 1.0 });

// 在动画循环中更新
function animate(time) {
  snowManager.update(time);
  renderer.render(scene, camera);
}

总结

技术点 实现方式
高性能渲染 InstancedMesh + GPU Instancing
无限循环 mod 取模 + 相机位置跟随
广告牌效果 viewMatrix 空间中偏移顶点
边缘渐隐 smoothstep + 距离衰减
雨滴折射 FBO 背景采样 + UV 偏移

通过这套方案,可以在 单个 Draw Call 内渲染数万级粒子,同时保持 60fps 的流畅体验。


源码地址

GitHub: Meteor3D

查看效果:meteor3d.cn

欢迎 Star ⭐ 和 Fork 🍴!


如果这篇文章对你有帮助,请点个赞 👍

相关推荐
我的写法有点潮4 小时前
JS中对象是怎么运算的呢
前端·javascript·面试
悠哉摸鱼大王4 小时前
NV12 转 RGB 完整指南
前端·javascript
一壶纱4 小时前
UniApp + Pinia 数据持久化
前端·数据库·uni-app
双向334 小时前
【RAG+LLM实战指南】如何用检索增强生成破解AI幻觉难题?
前端
海云前端14 小时前
前端人必懂的浏览器指纹:不止是技术,更是求职加分项
前端
青莲8434 小时前
Java内存模型(JMM)与JVM内存区域完整详解
android·前端·面试
parade岁月4 小时前
把 Git 提交变成“可执行规范”:Commit 规范体系与 Husky/Commitlint/Commitizen/Lint-staged 全链路介绍
前端·代码规范
青莲8434 小时前
Java内存回收机制(GC)完整详解
java·前端·面试
pas1364 小时前
29-mini-vue element搭建更新
前端·javascript·vue.js
IT=>小脑虎4 小时前
2026版 React 零基础小白进阶知识点【衔接基础·企业级实战】
前端·react.js·前端框架