本文介绍如何使用 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;
}
关键技巧解读:
- mod 取模运算 - 让粒子在固定范围内循环,超出边界自动回到另一侧
- 相机位置跟随 - 循环范围始终以相机为中心,玩家移动时雪花跟随
- 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 技术)
要实现雨滴的透明折射效果,需要:
- 先渲染背景到 RenderTarget(FBO)
- 雨滴采样 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 🍴!
如果这篇文章对你有帮助,请点个赞 👍