Three.js TSL实战:5分钟打造酷炫粒子鼠标跟随效果(附完整代码)
Three.js TSL实战:5分钟打造酷炫粒子鼠标跟随效果(附完整代码)
在Web 3D开发领域,Three.js一直是前端工程师的首选工具库。而随着WebGPU技术的普及,Three.js团队推出的TSL(Three.js Shading Language)为开发者提供了更高效的着色器编写方式。本文将带你快速实现一个令人惊艳的粒子鼠标跟随效果,不仅视觉效果出众,代码也极具复用价值。
1. 环境准备与基础配置
首先确保你的开发环境已经准备好。我们需要使用Three.js的最新版本(r176+),它原生支持TSL语法。创建一个基础的HTML文件,并添加以下依赖:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>TSL粒子效果</title> <style> body { margin: 0; overflow: hidden; } canvas { display: block; } </style> </head> <body> <script type="importmap"> { "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.176.0/build/three.module.js", "three/tsl": "https://cdn.jsdelivr.net/npm/three@0.176.0/build/three.tsl.js", "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.176.0/examples/jsm/" } } </script> <script type="module" src="app.js"></script> </body> </html>提示:使用importmap可以避免路径问题,确保所有依赖都能正确加载。
接下来创建app.js文件,初始化基础场景:
import * as THREE from 'three'; import { Fn, If, uniform, float, vec3, instancedArray, instanceIndex } from 'three/tsl'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // 初始化场景、相机和渲染器 const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // 添加轨道控制器方便调试 const controls = new OrbitControls(camera, renderer.domElement); camera.position.set(0, 0, 50); controls.update();2. 创建粒子系统核心逻辑
TSL的强大之处在于可以用JavaScript风格的语法编写着色器代码。我们将创建50万个粒子,它们会对鼠标移动做出反应。
// 粒子数量 - 50万 const PARTICLE_COUNT = 500000; // 创建粒子位置和速度的实例化数组 const positions = instancedArray(PARTICLE_COUNT, 'vec3'); const velocities = instancedArray(PARTICLE_COUNT, 'vec3'); const colors = instancedArray(PARTICLE_COUNT, 'vec3'); // 初始化粒子位置 const initParticles = Fn(() => { const position = positions.element(instanceIndex); const color = colors.element(instanceIndex); // 在球面上随机分布粒子 const radius = float(20).mul(hash(instanceIndex.add(1))); const theta = hash(instanceIndex).mul(Math.PI * 2); const phi = hash(instanceIndex.add(2)).mul(Math.PI); position.x = radius.mul(theta.sin()).mul(phi.cos()); position.y = radius.mul(phi.sin()); position.z = radius.mul(theta.cos()).mul(phi.cos()); // 随机颜色 color.assign(vec3( hash(instanceIndex), hash(instanceIndex.add(3)), hash(instanceIndex.add(5)) )); })().compute(PARTICLE_COUNT);粒子物理更新逻辑是效果的核心,我们使用TSL编写:
// 物理参数 const gravity = uniform(-0.0005); const bounce = uniform(0.7); const friction = uniform(0.98); const mouseForce = uniform(5); const mousePosition = uniform(new THREE.Vector3(0, 0, 0)); // 粒子更新函数 const updateParticles = Fn(() => { const position = positions.element(instanceIndex); const velocity = velocities.element(instanceIndex); // 应用重力 velocity.y = velocity.y.add(gravity); // 鼠标吸引力 const direction = position.sub(mousePosition).normalize(); const distance = position.distance(mousePosition); const force = float(1).div(distance.add(0.1)).mul(mouseForce); velocity.x = velocity.x.add(direction.x.mul(force)); velocity.y = velocity.y.add(direction.y.mul(force)); velocity.z = velocity.z.add(direction.z.mul(force)); // 更新位置 position.addAssign(velocity); velocity.mulAssign(friction); // 地面碰撞检测 If(position.y.lessThan(-20), () => { position.y = -20; velocity.y = velocity.y.negate().mul(bounce); velocity.x = velocity.x.mul(0.9); velocity.z = velocity.z.mul(0.9); }); })().compute(PARTICLE_COUNT);3. 渲染粒子与鼠标交互
现在我们需要将计算好的粒子渲染到屏幕上,并添加鼠标交互:
// 创建粒子材质 const particleMaterial = new THREE.PointsMaterial({ size: 0.1, vertexColors: true, transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending }); // 创建粒子几何体 const particleGeometry = new THREE.BufferGeometry(); particleGeometry.setAttribute('position', positions.toAttribute()); particleGeometry.setAttribute('color', colors.toAttribute()); // 创建粒子系统 const particles = new THREE.Points(particleGeometry, particleMaterial); scene.add(particles); // 鼠标移动交互 const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); function onMouseMove(event) { // 将鼠标坐标归一化为-1到1的范围 mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; // 更新射线投射器 raycaster.setFromCamera(mouse, camera); // 计算鼠标在3D空间中的位置 const intersects = raycaster.intersectObjects([new THREE.Mesh( new THREE.PlaneGeometry(1000, 1000), new THREE.MeshBasicMaterial({ visible: false }) )]); if (intersects.length > 0) { mousePosition.value.copy(intersects[0].point); mousePosition.value.z = 0; // 限制在XY平面 } } window.addEventListener('mousemove', onMouseMove);4. 动画循环与性能优化
最后设置动画循环,确保粒子系统流畅运行:
function animate() { requestAnimationFrame(animate); // 更新粒子位置 renderer.compute(updateParticles); // 更新几何体属性 particleGeometry.attributes.position.needsUpdate = true; // 渲染场景 renderer.render(scene, camera); controls.update(); } // 处理窗口大小变化 function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } window.addEventListener('resize', onWindowResize); animate();注意:对于大量粒子,建议使用WebGPU渲染器以获得更好性能。只需将WebGLRenderer替换为WebGPURenderer即可。
5. 进阶效果与自定义
基础效果实现后,你可以通过调整参数获得不同视觉效果:
- 参数调优表:
| 参数 | 默认值 | 效果说明 |
|---|---|---|
| gravity | -0.0005 | 重力大小,负值表示向下 |
| bounce | 0.7 | 碰撞反弹系数 |
| friction | 0.98 | 摩擦力,值越小减速越快 |
| mouseForce | 5 | 鼠标吸引力强度 |
| particleSize | 0.1 | 粒子显示大小 |
尝试修改这些值,观察效果变化。例如,增加mouseForce会让粒子对鼠标移动更敏感:
// 更强的鼠标交互 mouseForce.value = 10;你还可以添加更多视觉效果,如:
// 添加辉光效果 import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; const composer = new EffectComposer(renderer); composer.addPass(new UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85 )); // 在animate()中替换renderer.render为composer.render通过组合不同的参数和后期处理效果,你可以创造出独一无二的粒子动画。在实际项目中,这种技术可应用于数据可视化、游戏特效或网页背景等多个领域。
