学习Three.js–雪花

0 阅读25分钟

学习Three.js--雪花

前置核心说明

开发目标

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

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

c8649201-4fa3-4f22-96f9-567c677a3c84.png

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

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

分步开发详解

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

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

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

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

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

3.1 核心代码
// 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 核心代码
// 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 核心代码
// 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 核心代码
// 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 核心代码
// 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. 雪花视觉效果的核心实现(着色器逻辑)

雪花的柔和光晕与圆形形态是通过片元着色器的四层逻辑实现的,每层逻辑环环相扣,最终还原真实雪花的视觉特征,具体流程如下:

graph LR
    A[获取粒子内UV坐标 gl_PointCoord] --> B[计算到中心的距离 distanceToCenter]
    B --> C[smoothstep实现边缘渐隐 alpha]
    C --> D[pow放大亮度差异 brightness]
    D --> E[组合颜色与透明度 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的自定义视觉逻辑,无需额外的几何体与纹理资源,进一步提升渲染效率。

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

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

完整代码

<!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,提升渲染效率。