学习Three.js–雪花

学习Three.js--雪花

前置核心说明

开发目标

基于Three.js的粒子系统+自定义着色器实现真实感3D雪花飘落效果,还原雪花的自然视觉与动态特征,核心能力包括:

  1. 模拟雪花的视觉形态:通过圆形抗锯齿粒子+中心亮边缘暗的光晕,还原雪花在暗光环境下的柔和反光效果;
  2. 实现自然飘落动画:垂直下落+水平随机偏移,避免雪花运动轨迹过于规整,还原真实雪花的飘落姿态;
  3. 构建循环动画逻辑:雪花超出下边界后自动重置到顶部,实现无限循环的雪花飘落效果,无粒子缺失;
  4. 借助ShaderMaterial提升视觉质感:启用加法混合让雪花重叠处光晕更柔和,兼顾性能与视觉细腻度;
  5. 支持轨道交互查看:通过OrbitControls实现360°拖拽旋转、滚轮缩放,全方位观察3D雪花群的飘落效果。

核心技术栈(关键知识点)

技术点 作用
THREE.BufferGeometry + Float32Array 高效存储1000级雪花粒子坐标数据,减少CPU与GPU数据传输开销,支撑流畅渲染
自定义粒子视觉算法(着色器实现) 1. 实现圆形抗锯齿雪花粒子,替代默认方形粒子;2. 打造"中心亮、边缘暗"的柔和光晕,还原雪花反光特性;3. 传递全局统一颜色/尺寸,实现雪花视觉一致性
THREE.ShaderMaterial(顶点/片元着色器) 运行在GPU上,具备并行处理能力,高效实现雪花的像素级视觉效果,兼顾性能与质感
THREE.AdditiveBlending(加法混合) 雪花重叠处亮度叠加,营造朦胧柔和的光晕感,模拟雪花群的视觉层次感,避免粒子重叠生硬遮挡
雪花双动态逻辑(垂直下落+水平偏移) 微观雪花垂直下落+水平随时间偏移,宏观实现循环重置,营造自然且持久的雪花飘落氛围
THREE.OrbitControls(轨道控制器) 支持拖拽旋转/滚轮缩放,全方位查看3D雪花群的飘落效果,便捷观察雪花的光晕与运动轨迹

核心开发流程

初始化场景/相机/渲染器/控制器
雪花粒子核心配置(数量/尺寸/颜色)
编写自定义着色器(顶点/片元,实现雪花视觉效果)
构建ShaderMaterial(配置uniforms+混合模式,优化视觉质感)
构建BufferGeometry(绑定雪花粒子坐标数据)
创建Points粒子对象并添加到场景
实现雪花更新函数(下落+偏移+循环重置)
动画循环(更新时间+粒子更新+控制器阻尼+场景渲染)
窗口适配+持续渲染


分步开发详解

步骤1:基础环境搭建(场景/相机/渲染器/控制器)

搭建Three.js 3D场景的基础框架,为雪花飘落效果提供展示载体,保证场景的流畅交互与高清渲染,贴合雪花的暗光视觉氛围。

1.1 核心代码
javascript 复制代码
// 1. 导入核心库与轨道控制器
import * as THREE from 'https://esm.sh/three@0.174.0';
import { OrbitControls } from 'https://esm.sh/three@0.174.0/examples/jsm/controls/OrbitControls.js';

// 2. 基础环境初始化(场景/相机/渲染器)
const scene = new THREE.Scene();

// 透视相机:配置合理参数,确保能完整观察粒子群
const camera = new THREE.PerspectiveCamera(
  40,                     // 视角(FOV):40°视野适中,无雪花粒子变形
  window.innerWidth / window.innerHeight, // 宽高比:适配浏览器窗口
  1,                      // 近裁切面:过滤过近无效对象
  10000                   // 远裁切面:保证雪花群完整处于可见范围
);
// 相机初始位置:(0, 0, 200) 正面平视,清晰观察雪花飘落的纵深与水平偏移效果
camera.position.set(0, 0, 200);

// 渲染器:开启抗锯齿,提升雪花粒子边缘细腻度,避免光晕锯齿感
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio); // 高清适配Retina屏幕,雪花光晕无模糊
document.body.appendChild(renderer.domElement);

// 轨道控制器:支持拖拽旋转/滚轮缩放,便捷观察3D雪花群
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 启用阻尼:拖拽旋转有惯性,交互更顺滑自然
1.2 关键说明
  • 相机位置(0, 0, 200) 采用「正面平视+稍远」视角,既可以完整捕捉雪花群的飘落纵深效果,又能清晰观察单朵雪花的光晕细节,避免视角过近导致雪花光晕过曝、无法观察整体飘落态势。
  • 渲染器antialias: true :开启抗锯齿,配合后续片元着色器的smoothstep抗锯齿逻辑,让雪花粒子的边缘和光晕更细腻------这对白色雪花尤为重要,可避免雪花出现"锯齿边",提升暗光环境下的视觉柔和度。
  • 控制器阻尼:启用阻尼后,拖拽旋转查看雪花群时,场景会有自然的惯性减速,更贴合3D场景的交互体验,便于长时间观察雪花的飘落轨迹与光晕叠加效果。

步骤2:雪花粒子核心配置(数量/视觉参数)

定义雪花粒子的基础配置参数,为后续着色器与几何体构建提供数据支撑,保证雪花群的密集度与视觉一致性。

2.1 核心代码
javascript 复制代码
// 3. 粒子系统核心配置
const count = 1000; // 雪花总数:保证飘落效果的浓密感,同时兼顾低配设备流畅性
2.2 关键说明
  • count参数的平衡:1000是"视觉效果"与"性能"的最优平衡值------少于500会导致雪花过于稀疏,缺乏漫天飘落的氛围感;多于2000会增加GPU渲染开销,低配设备可能出现卡顿,尤其在开启加法混合后,粒子重叠计算量会增加。
  • 雪花的视觉预设 :本步骤仅定义粒子数量,后续通过着色器统一配置雪花的尺寸(pointSize)与颜色(uColor),保证所有雪花视觉风格一致,还原真实雪花的统一性。

步骤3:自定义着色器(雪花视觉效果的核心)

通过顶点着色器与片元着色器,实现雪花的圆形形态、柔和光晕、抗锯齿边缘,这是雪花视觉质感的核心,所有逻辑运行在GPU上,高效且细腻。

3.1 核心代码
javascript 复制代码
// 4. 自定义着色器(顶点着色器 + 片元着色器)
// 顶点着色器:处理雪花位置、尺寸,完成3D→2D透视变换
const vertexShader = `
  uniform float pointSize; // 全局统一雪花尺寸(uniform:所有雪花共享)
  void main() {
    // 核心:透视变换链,将雪花局部坐标转换为屏幕裁剪坐标,3D渲染必备
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    gl_PointSize = pointSize; // 为每个雪花设置屏幕尺寸,保证视觉一致性
  }
`;

// 片元着色器:处理雪花像素级视觉效果(形状、颜色、透明度、亮度)
const fragmentShader = `
  uniform vec3 uColor; // 全局统一雪花颜色(uniform:所有雪花共享)
  void main() {
    // 步骤1:计算当前像素到雪花中心的距离(gl_PointCoord:粒子内UV坐标,范围(0,0)~(1,1))
    float distanceToCenter = distance(gl_PointCoord, vec2(0.5));
    
    // 步骤2:边缘渐隐抗锯齿,避免雪花边缘生硬锯齿,提升光晕柔和度
    // smoothstep(0.4, 0.5, d):d<0.4返回0,d>0.5返回1,中间平滑插值
    float alpha = 1.0 - smoothstep(0.4, 0.5, distanceToCenter);
    
    // 步骤3:增强中心亮度,实现"中心亮、边缘暗"的光晕效果,还原雪花反光特性
    // pow(指数5.0):放大亮度差异,让中心更亮,边缘更暗,光晕更有层次感
    float brightness = pow(1.0 - distanceToCenter, 5.0);
    
    // 步骤4:设置最终像素颜色(基础色*亮度 + 渐隐透明度)
    gl_FragColor = vec4(uColor * brightness, alpha);
  }
`;
3.2 关键技术点解析
  1. 顶点着色器的核心职责

    • 完成3D坐标到2D屏幕坐标的透视变换链projectionMatrix * modelViewMatrix * vec4(position, 1.0),这是Three.js 3D渲染的通用逻辑,保证雪花在屏幕上的正确显示与"近大远小"的透视效果。
    • 统一设置雪花尺寸:通过uniform变量pointSize为所有雪花设置相同的屏幕尺寸,保证视觉一致性,避免单朵雪花过大或过小破坏整体效果。
  2. 片元着色器的雪花视觉实现

    • gl_PointCoord:雪花粒子内的UV坐标,范围从(0,0)(左下角)到(1,1)(右上角),vec2(0.5)对应雪花中心,通过计算像素到中心的距离,实现圆形雪花形态。
    • distance():计算欧几里得距离,获取当前像素与雪花中心的间距,为后续圆形裁剪与光晕实现提供数据支撑。
    • smoothstep():实现边缘渐隐,避免圆形雪花出现生硬锯齿------这是雪花视觉柔和度的关键,让雪花边缘从亮到暗平滑过渡,形成自然光晕。
    • pow():幂指数5.0放大亮度差异,让雪花中心更明亮(模拟雪花的反光核心),边缘更暗淡(模拟光晕的衰减),还原真实雪花在暗光环境下的视觉特征。

步骤4:构建着色器材质(连接着色器,优化视觉效果)

将自定义着色器与Three.js的ShaderMaterial绑定,配置全局uniform参数与渲染模式,实现雪花的透明光晕与叠加效果,提升视觉层次感。

4.1 核心代码
javascript 复制代码
// 5. 构建着色器材质(连接两个着色器,配置参数与渲染模式)
const material = new THREE.ShaderMaterial({
  vertexShader: vertexShader,   // 绑定顶点着色器
  fragmentShader: fragmentShader, // 绑定片元着色器
  uniforms: {                    // 向着色器传递的全局统一参数
    pointSize: { value: 10.0 },  // 雪花屏幕尺寸:10.0兼顾细腻度与可见度
    uColor: { value: new THREE.Color(0xffffff) } // 雪花基础颜色:纯白色,还原真实雪花
  },
  transparent: true,             // 启用透明:支持alpha通道,实现边缘渐隐光晕
  depthTest: true,               // 启用深度测试:避免远雪花遮挡近雪花,保证正常渲染层级
  depthWrite: false,             // 优化:关闭深度写入,避免透明雪花互相遮挡,光晕更连贯
  blending: THREE.AdditiveBlending // 优化:启用加法混合,雪花重叠处亮度叠加,提升光晕质感
});
4.2 关键说明
  • 全局uniform参数pointSizeuColor是所有雪花共享的全局参数,后续可通过修改material.uniforms.xxx.value实现雪花尺寸与颜色的动态调整(如随时间变浅/变大)。
  • transparent: true :启用透明通道,支持片元着色器中的alpha值,实现雪花边缘的渐隐效果------如果关闭该参数,雪花会变成不透明的白色方块,丢失光晕质感。
  • depthWrite: false:关闭深度写入,允许透明雪花互相叠加,避免"后渲染的雪花被先渲染的雪花遮挡"的问题,让雪花群的光晕更连贯,尤其在雪花密集区域,视觉效果更柔和。
  • AdditiveBlending(加法混合):雪花重叠处的亮度会叠加,形成更明亮的白色光晕,模拟漫天雪花飘落的朦胧氛围------如果关闭该参数,雪花重叠区域会显得灰暗,缺乏氛围感。

步骤5:构建BufferGeometry(高效存储雪花坐标数据)

通过Float32ArrayBufferGeometry高效存储雪花的3D坐标数据,减少CPU与GPU之间的数据传输开销,支撑1000级粒子的流畅渲染。

5.1 核心代码
javascript 复制代码
// 6. 构建粒子几何体(BufferGeometry:高效存储大量顶点数据)
// 创建浮点型数组存储雪花顶点坐标(每个雪花3个值:x/y/z,共count*3个元素)
const positions = new Float32Array(count * 3);
for (let i = 0; i < count * 3; i += 3) {
  // 雪花坐标范围:(-100, -100, -100) ~ (100, 100, 100),均匀分布在3D空间中
  positions[i]     = Math.random() * 200 - 100; // x轴坐标:水平左右分布
  positions[i + 1] = Math.random() * 200 - 100; // y轴坐标:垂直上下分布(用于下落动画)
  positions[i + 2] = Math.random() * 200 - 100; // z轴坐标:纵深前后分布,提升3D感
}

// 初始化BufferGeometry,绑定顶点位置数据
const geometry = new THREE.BufferGeometry();
// Float32BufferAttribute(数据数组, 每个顶点的分量数):此处3对应x/y/z
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));

// 7. 构建粒子对象(THREE.Points:专门用于渲染点粒子系统)
const mesh = new THREE.Points(geometry, material);
scene.add(mesh); // 将雪花群添加到场景中
5.2 关键技术点解析
  • Float32Array的高效性:这是一种二进制浮点数组,相比普通JS数组,占用内存更少(每个元素仅4字节)、传输到GPU的速度更快,专门用于存储大量粒子的坐标数据,是高性能粒子系统的必备选择。
  • BufferGeometry的优势 :Three.js推荐的高性能几何体,相比已废弃的Geometry,它直接将数据存储在GPU显存中,减少CPU与GPU之间的频繁数据传输,能够轻松支撑1000级甚至万级粒子的流畅渲染。
  • 雪花的3D空间分布 :x/y/z三轴均在(-100, 100)范围内随机分布,形成立体的雪花群------z轴的纵深分布让雪花飘落时呈现"近快远慢"的透视效果,提升3D场景的真实感,避免平面化的飘落效果。
  • THREE.Points :专门用于渲染点粒子系统的对象,将BufferGeometry(坐标数据)与ShaderMaterial(视觉效果)结合,生成最终的雪花群,相比Mesh对象,它的渲染开销更低,更适合大量粒子场景。

步骤6:雪花动态更新函数(实现自然飘落与循环动画)

编写粒子更新逻辑,实现雪花的垂直下落、水平随机偏移,以及超出边界后的重置,打造无限循环的自然飘落效果,避免雪花消失后场景空洞。

6.1 核心代码
javascript 复制代码
// 8. 粒子动态更新函数(实现下落+偏移动画,重置超出边界的雪花)
function update(time) {
  if (!material || !mesh) return; // 安全判断:避免对象未初始化报错
  
  // 获取雪花顶点位置数组(geometry.getAttribute返回的是属性对象,.array获取原始数据)
  const positions = mesh.geometry.getAttribute("position").array;
  
  // 遍历所有雪花,更新坐标(每3个元素对应一个雪花的x/y/z)
  for (let i = 0; i < positions.length; i += 3) {
    // 步骤1:垂直下落(y轴递减,速度0.2):模拟雪花的重力下落
    positions[i + 1] -= 0.2;
    
    // 步骤2:水平轻微偏移(结合时间参数,让偏移随时间变化,运动更自然)
    // 原始代码Math.sin(i)是固定值,优化为Math.sin(i + time),实现动态左右/前后偏移
    positions[i]     -= Math.sin(i + time) * 0.1;   // x轴偏移:左右晃动
    positions[i + 2] -= Math.sin(i + time * 0.8) * 0.1; // z轴偏移:前后晃动,避免偏移同步
    
    // 步骤3:重置超出下边界的雪花(y < -100时,重置到顶部100,实现循环下落)
    if (positions[i + 1] < -100) {
      positions[i + 1] = 100;
      // 重置时同步更新x/z坐标,避免雪花重置后位置单一,提升自然感
      positions[i]     = Math.random() * 200 - 100;
      positions[i + 2] = Math.random() * 200 - 100;
    }
  }
  
  // 关键:标记位置属性需要更新,告诉Three.js重新上传数据到GPU
  mesh.geometry.getAttribute("position").needsUpdate = true;
}
6.2 关键技术点解析
  1. 自然飘落的双动效逻辑

    • 垂直下落positions[i + 1] -= 0.2(y轴递减),模拟雪花受重力下落的效果,0.2的速度兼顾流畅度与视觉舒适度------速度过快会显得杂乱,过慢会缺乏动感。
    • 水平/纵深偏移 :通过Math.sin(i + time)实现动态偏移,i保证每个雪花的偏移相位不同,time保证偏移随时间变化,0.8的时间系数让z轴偏移与x轴不同步,避免雪花"整齐划一"的晃动,还原真实雪花的无规则飘落轨迹。
  2. 循环动画的核心逻辑

    • 边界判断:positions[i + 1] < -100,当雪花下落至下边界以下时,触发重置逻辑。
    • 顶部重置:将雪花的y轴坐标重置为100(顶部),同时重新随机生成x/z轴坐标,避免雪花在同一位置重复出现,提升飘落效果的自然感。
    • needsUpdate = true:这是动态更新粒子坐标的关键------修改positions数组后,必须标记位置属性需要更新,否则Three.js不会将新数据上传到GPU,雪花将无法实现下落与重置动画。

步骤7:动画循环与窗口适配(驱动动画,适配不同屏幕)

通过requestAnimationFrame驱动动画循环,更新雪花位置与控制器阻尼,同时实现窗口响应式适配,保证雪花效果在不同屏幕尺寸下正常显示。

7.1 核心代码
javascript 复制代码
// 9. 动画循环(驱动粒子动画,实现流畅渲染)
function animate() {
  requestAnimationFrame(animate); // 绑定浏览器刷新率(60帧/秒),避免卡顿
  const time = Date.now() * 0.0005; // 获取当前时间(缩放系数0.0005,让动画速度适中)
  
  update(time); // 调用雪花更新函数,传递时间参数
  controls.update(); // 更新轨道控制器阻尼(必须在动画循环中调用)
  renderer.render(scene, camera); // 渲染场景(将3D雪花群转换为2D画布显示)
}

// 启动动画循环
animate();

// 10. 窗口响应式适配(适配不同屏幕尺寸,避免雪花拉伸变形)
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight; // 更新相机宽高比
  camera.updateProjectionMatrix(); // 关键:更新相机投影矩阵,让宽高比修改生效
  renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染器尺寸
});
7.2 关键说明
  • requestAnimationFrame :绑定浏览器的刷新率(通常为60帧/秒),相比setInterval,它能避免动画卡顿与掉帧,保证雪花飘落效果的流畅性,同时节省CPU与GPU资源。
  • 时间缩放系数0.0005Date.now()返回的是毫秒级时间戳,直接使用会导致动画速度过快,通过* 0.0005将时间缩放为秒级且放缓速度,让雪花的偏移与下落更自然。
  • camera.updateProjectionMatrix():窗口尺寸变化时,相机的宽高比会修改,必须调用该方法更新投影矩阵,否则雪花群会出现拉伸变形,破坏3D视觉效果。

核心技术深度解析

1. 雪花视觉效果的核心实现(着色器逻辑)

雪花的柔和光晕与圆形形态是通过片元着色器的四层逻辑实现的,每层逻辑环环相扣,最终还原真实雪花的视觉特征,具体流程如下:
获取粒子内UV坐标 gl_PointCoord
计算到中心的距离 distanceToCenter
smoothstep实现边缘渐隐 alpha
pow放大亮度差异 brightness
组合颜色与透明度 gl_FragColor

  • 核心亮点:通过像素级计算实现视觉效果,运行在GPU上,具备并行处理能力,即使1000级雪花,也能保持60帧流畅渲染,这是普通JS逻辑无法实现的。
  • 关键优化:AdditiveBlending加法混合让雪花重叠处亮度叠加,形成朦胧的光晕氛围,模拟漫天飞雪的视觉效果,同时depthWrite: false避免透明粒子互相遮挡,保证光晕的连贯性。

2. 雪花飘落动画的循环逻辑(CPU端动态更新)

雪花的无限循环飘落是通过"下落-判断-重置"的闭环逻辑实现的,核心要点如下:

步骤 逻辑 关键代码 作用
1 垂直下落 positions[i + 1] -= 0.2 模拟重力下落,实现雪花的基础运动轨迹
2 动态偏移 positions[i] -= Math.sin(i + time) * 0.1 实现无规则水平/纵深晃动,提升自然感
3 边界判断 if (positions[i + 1] < -100) 检测雪花是否超出下边界,触发重置逻辑
4 顶部重置 positions[i + 1] = 100 将雪花重置到顶部,实现无限循环
5 数据更新 needsUpdate = true 通知Three.js上传新数据到GPU,动画生效

3. BufferGeometryShaderMaterial的性能优势

本代码能够高效支撑1000级粒子渲染,核心在于BufferGeometryShaderMaterial的组合,其性能优势主要体现在:

  1. 数据高效存储BufferGeometry使用二进制数组存储粒子数据,一次性上传到GPU显存,后续仅需更新少量数据(如位置属性),减少CPU与GPU之间的频繁数据传输。
  2. GPU并行处理:着色器逻辑运行在GPU上,具备强大的并行处理能力,可同时处理上千个雪花的视觉渲染,性能远超普通JS驱动的粒子系统。
  3. 低渲染开销THREE.Points对象的渲染开销远低于Mesh对象,配合ShaderMaterial的自定义视觉逻辑,无需额外的几何体与纹理资源,进一步提升渲染效率。

核心参数速查表(快速调整雪花效果)

参数分类 参数名 当前取值 核心作用 修改建议
雪花基础配置 count 1000 雪花总数,决定飘落效果的浓密感 改为500:雪花更稀疏,低配设备更流畅;改为2000:雪花更浓密,氛围感更强(需注意性能)
雪花视觉配置 pointSize 10.0 雪花屏幕尺寸,决定单朵雪花的大小 改为5.0:雪花更小更细腻,模拟细雪;改为20.0:雪花更大更醒目,模拟鹅毛大雪
雪花视觉配置 uColor new THREE.Color(0xffffff) 雪花基础颜色,决定雪花的整体色调 改为0xf0f8ff(淡蓝色):模拟寒冬冷雪;改为0xfffff0(米白色):模拟暖光下的雪花
雪花动画配置 下落速度 positions[i + 1] -= 0.2 0.2 雪花垂直下落的速度,决定动画节奏 改为0.1:下落更慢,节奏更舒缓;改为0.5:下落更快,动态感更强
雪花动画配置 偏移幅度 * 0.1 0.1 雪花水平/纵深偏移的幅度,决定晃动程度 改为0.05:偏移更小,飘落更平稳;改为0.2:偏移更大,飘落更杂乱,模拟大风天气
动画配置 时间缩放系数 * 0.0005 0.0005 动画的整体速度系数,决定偏移的快慢 改为0.0003:偏移更慢,更自然;改为0.001:偏移更快,更动感
相机配置 camera.position.set(0, 0, 200) 200(z轴距离) 相机与雪花群的距离,决定观察视角 改为150:相机更近,雪花更大;改为300:相机更远,可观察雪花群的整体飘落态势

完整代码

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>雪花 - Three.js</title>
  <style>
    body { margin: 0; overflow: hidden; background: #000; }
  </style>
</head>
<body>
  <script type="module">
  // 1. 导入核心库与轨道控制器
  import * as THREE from 'https://esm.sh/three@0.174.0';
  import { OrbitControls } from 'https://esm.sh/three@0.174.0/examples/jsm/controls/OrbitControls.js';

  // 2. 基础环境初始化(场景/相机/渲染器)
  const scene = new THREE.Scene();

  // 透视相机:配置合理参数,确保能完整观察粒子群
  const camera = new THREE.PerspectiveCamera(
    40,                     // 视角(FOV):40°视野适中,无粒子变形
    window.innerWidth / window.innerHeight, // 宽高比:适配浏览器窗口
    1,                      // 近裁切面:过滤过近无效对象
    10000                   // 远裁切面:保证粒子群完整处于可见范围
  );
  // 修复:设置相机初始位置(原始代码缺失,导致无法看到粒子)
  camera.position.set(0, 0, 200);

  // 渲染器:开启抗锯齿,提升粒子边缘细腻度
  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(window.devicePixelRatio); // 高清适配Retina屏幕
  document.body.appendChild(renderer.domElement);

  // 3. 粒子系统核心配置
  const count = 1000; // 粒子总数

  // 4. 自定义着色器(顶点着色器 + 片元着色器)
  // 顶点着色器:处理粒子位置、尺寸,完成3D→2D透视变换
  const vertexShader = `
    uniform float pointSize; // 全局统一粒子尺寸(uniform:所有粒子共享)
    void main() {
      // 核心:透视变换链,将粒子局部坐标转换为屏幕裁剪坐标
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      gl_PointSize = pointSize; // 为每个粒子设置屏幕尺寸
    }
  `;

  // 片元着色器:处理粒子像素级视觉效果(形状、颜色、透明度、亮度)
  const fragmentShader = `
    uniform vec3 uColor; // 全局统一粒子颜色(uniform:所有粒子共享)
    void main() {
      // 步骤1:计算当前像素到粒子中心的距离(gl_PointCoord:粒子内UV坐标,范围(0,0)~(1,1))
      float distanceToCenter = distance(gl_PointCoord, vec2(0.5));
      
      // 步骤2:边缘渐隐抗锯齿,避免粒子边缘生硬锯齿
      // smoothstep(0.4, 0.5, d):d<0.4返回0,d>0.5返回1,中间平滑插值
      float alpha = 1.0 - smoothstep(0.4, 0.5, distanceToCenter);
      
      // 步骤3:增强中心亮度,实现"中心亮、边缘暗"的光晕效果
      // pow(指数5.0):放大亮度差异,让中心更亮,光晕更有层次感
      float brightness = pow(1.0 - distanceToCenter, 5.0);
      
      // 步骤4:设置最终像素颜色(基础色*亮度 + 渐隐透明度)
      gl_FragColor = vec4(uColor * brightness, alpha);
    }
  `;

  // 5. 构建着色器材质(连接两个着色器,配置参数与渲染模式)
  const material = new THREE.ShaderMaterial({
    vertexShader: vertexShader,   // 绑定顶点着色器
    fragmentShader: fragmentShader, // 绑定片元着色器
    uniforms: {                    // 向着色器传递的全局统一参数
      pointSize: { value: 10.0 },  // 粒子屏幕尺寸
      uColor: { value: new THREE.Color(0xffffff) } // 粒子基础颜色(白色)
    },
    transparent: true,             // 启用透明:支持alpha通道,实现边缘渐隐
    depthTest: true,               // 启用深度测试:避免远粒子遮挡近粒子(正常渲染层级)
    depthWrite: false,             // 优化:关闭深度写入,避免透明粒子互相遮挡,光晕更连贯
    blending: THREE.AdditiveBlending // 优化:启用加法混合,粒子重叠处亮度叠加,提升光晕质感
  });

  // 6. 构建粒子几何体(BufferGeometry:高效存储大量顶点数据)
  // 创建浮点型数组存储粒子顶点坐标(每个粒子3个值:x/y/z,共count*3个元素)
  const positions = new Float32Array(count * 3);
  for (let i = 0; i < count * 3; i += 3) {
    // 粒子坐标范围:(-100, -100, -100) ~ (100, 100, 100),均匀分布在空间中
    positions[i]     = Math.random() * 200 - 100; // x轴坐标
    positions[i + 1] = Math.random() * 200 - 100; // y轴坐标(垂直方向,用于下落动画)
    positions[i + 2] = Math.random() * 200 - 100; // z轴坐标
  }

  // 初始化BufferGeometry,绑定顶点位置数据
  const geometry = new THREE.BufferGeometry();
  // Float32BufferAttribute(数据数组, 每个顶点的分量数):此处3对应x/y/z
  geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));

  // 7. 构建粒子对象(THREE.Points:专门用于渲染点粒子系统)
  const mesh = new THREE.Points(geometry, material);
  scene.add(mesh); // 将粒子对象添加到场景中

  // 8. 轨道控制器:支持拖拽旋转/滚轮缩放,便捷观察3D粒子群
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true; // 启用阻尼:拖拽旋转有惯性,交互更顺滑

  // 9. 粒子动态更新函数(实现下落+偏移动画,重置超出边界的粒子)
  function update(time) {
    if (!material || !mesh) return; // 安全判断:避免对象未初始化报错
    
    // 获取粒子顶点位置数组(geometry.getAttribute返回的是属性对象,.array获取原始数据)
    const positions = mesh.geometry.getAttribute("position").array;
    
    // 遍历所有粒子,更新坐标(每3个元素对应一个粒子的x/y/z)
    for (let i = 0; i < positions.length; i += 3) {
      // 步骤1:垂直下落(y轴递减,速度0.2)
      positions[i + 1] -= 0.2;
      
      // 步骤2:水平轻微偏移(结合时间参数,让偏移随时间变化,运动更自然)
      // 原始代码Math.sin(i)是固定值,优化为Math.sin(i + time),实现动态偏移
      positions[i]     -= Math.sin(i + time) * 0.1;   // x轴偏移
      positions[i + 2] -= Math.sin(i + time * 0.8) * 0.1; // z轴偏移(不同时间系数,避免偏移同步)
      
      // 步骤3:重置超出下边界的粒子(y < -100时,重置到顶部100,实现循环下落)
      if (positions[i + 1] < -100) {
        positions[i + 1] = 100;
        // 重置时同步更新x/z坐标,避免粒子重置后位置单一
        positions[i]     = Math.random() * 200 - 100;
        positions[i + 2] = Math.random() * 200 - 100;
      }
    }
    
    // 关键:标记位置属性需要更新,告诉Three.js重新上传数据到GPU
    mesh.geometry.getAttribute("position").needsUpdate = true;
  }

  // 10. 动画循环(驱动粒子动画,实现流畅渲染)
  function animate() {
    requestAnimationFrame(animate); // 绑定浏览器刷新率(60帧/秒),避免卡顿
    const time = Date.now() * 0.0005; // 获取当前时间(缩放系数0.0005,让动画速度适中)
    
    update(time); // 调用粒子更新函数,传递时间参数
    controls.update(); // 更新轨道控制器阻尼(必须在动画循环中调用)
    renderer.render(scene, camera); // 渲染场景(将3D粒子群转换为2D画布显示)
  }

  // 启动动画循环
  animate();

  // 11. 窗口响应式适配(适配不同屏幕尺寸,避免粒子拉伸变形)
  window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight; // 更新相机宽高比
    camera.updateProjectionMatrix(); // 关键:更新相机投影矩阵,让宽高比修改生效
    renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染器尺寸
  });
  </script>
</body>
</html>

总结与扩展建议

核心总结

  1. 视觉核心 :通过片元着色器的distance()+smoothstep()+pow()逻辑,实现雪花的圆形抗锯齿与柔和光晕,配合AdditiveBlending加法混合,还原漫天飞雪的朦胧氛围感,这是雪花效果的核心亮点。
  2. 动态核心 :"垂直下落+动态偏移+循环重置"的闭环逻辑,实现雪花的自然飘落与无限循环,needsUpdate = true是动态更新的关键,保证动画流畅生效。
  3. 性能核心BufferGeometry+ShaderMaterial的组合,高效存储与渲染1000级粒子,借助GPU并行处理能力,在保证视觉质感的同时,兼顾低配设备的流畅性。
  4. 交互核心OrbitControls轨道控制器提供360°交互查看能力,让用户能够全方位观察3D雪花群的飘落效果,提升场景的沉浸感。

扩展建议

  1. 雪花效果增强
    • 自定义雪花形状:修改片元着色器,将圆形雪花改为六角形(通过UV坐标计算与裁剪实现),还原真实雪花的形态;
    • 动态尺寸/颜色:通过material.uniforms.uTime传递时间参数,让雪花的尺寸与颜色随时间轻微变化(如下落过程中逐渐变小、变浅),提升动态感;
    • 添加雪花反光:在片元着色器中添加光源计算,模拟雪花对环境光的反光,让雪花效果更真实。
  2. 场景氛围增强
    • 添加背景图:通过THREE.TextureLoader加载夜空/森林背景图,作为场景背景,提升漫天飞雪的氛围感;
    • 添加地面积雪:在场景中添加平面几何体,模拟地面,实现雪花落地堆积的效果;
    • 添加风力控制:通过鼠标位置或键盘事件,控制雪花的偏移方向与幅度,模拟不同风力下的雪花飘落效果。
  3. 功能扩展
    • 参数控制面板:提供可视化面板,允许用户调整雪花数量、尺寸、下落速度等参数,实时预览效果;
    • 相机自动漫游:实现相机的自动路径漫游,让用户无需手动操作,即可全方位观察雪花飘落效果;
    • 移动端适配:优化触摸交互,支持移动端的捏合缩放与拖拽旋转,提升移动端用户体验。
  4. 性能优化
    • 视锥体裁剪:剔除屏幕外的雪花粒子,减少GPU渲染开销,支持更多粒子数量;
    • 实例化几何体:使用InstancedBufferGeometry替代BufferGeometry,进一步减少DrawCall,支持百万级雪花粒子渲染;
    • 渲染器优化:开启renderer.powerPreference = "high-performance",优先使用高性能GPU,提升渲染效率。
相关推荐
onebyte8bits3 小时前
前端国际化(i18n)体系设计与工程化落地
前端·国际化·i18n·工程化
C澒4 小时前
前端分层架构实战:DDD 与 Clean Architecture 在大型业务系统中的落地路径与项目实践
前端·架构·系统架构·前端框架
BestSongC4 小时前
行人摔倒检测系统 - 前端文档(1)
前端·人工智能·目标检测
0思必得04 小时前
[Web自动化] Selenium处理滚动条
前端·爬虫·python·selenium·自动化
Misnice4 小时前
Webpack、Vite、Rsbuild区别
前端·webpack·node.js
青茶3604 小时前
php怎么实现订单接口状态轮询(二)
前端·php·接口
大橙子额5 小时前
【解决报错】Cannot assign to read only property ‘exports‘ of object ‘#<Object>‘
前端·javascript·vue.js
爱喝白开水a6 小时前
前端AI自动化测试:brower-use调研让大模型帮你做网页交互与测试
前端·人工智能·大模型·prompt·交互·agent·rag
董世昌416 小时前
深度解析ES6 Set与Map:相同点、核心差异及实战选型
前端·javascript·es6