Three.js 粒子系统与赛博朋克视觉特效:从几何到氛围的渲染实践

Three.js 粒子系统与赛博朋克视觉特效:从几何到氛围的渲染实践 Three.js 粒子系统与赛博朋克视觉特效从几何到氛围的渲染实践一、赛博朋克 UI 的氛围密码粒子不是装饰是叙事赛博朋克风格的 Web 体验中粒子系统不是锦上添花的装饰而是构建沉浸感的核心视觉语言。雨滴、数据流、全息投影碎片——这些动态元素传递着高科技、低生活的叙事张力。但粒子系统的实现远非生成一堆随机点那么简单。数万粒子同时运动时的帧率下降、粒子间缺乏物理一致性导致的廉价感、以及粒子生命周期管理不当导致的内存泄漏都是常见的工程陷阱。Three.js 的粒子系统基于 Points 和 BufferGeometry提供了 GPU 友好的批量渲染方案。但要实现赛博朋克风格的氛围感还需要自定义着色器Shader、后处理特效Post-processing和基于噪声的运动算法。二、粒子系统渲染管线与性能架构Three.js 粒子系统的渲染管线分为四个阶段粒子数据初始化 → 每帧位置更新 → GPU 批量渲染 → 后处理叠加。性能优化的核心原则是数据留在 GPU计算交给着色器——避免在 JavaScript 中逐粒子更新位置而是通过 Uniform 变量驱动 Shader 中的运动计算。flowchart LR A[CPU 端br/BufferGeometry 初始化] --|上传一次| B[GPU 显存br/Position / Color / Size] C[CPU 端br/每帧更新 Uniform] --|时间 / 鼠标位置| D[Vertex Shaderbr/计算新位置] B -- D D -- E[Fragment Shaderbr/颜色 / 透明度] E -- F[后处理br/Bloom / Glitch] F -- G[最终画面] subgraph 性能关键路径 D E end关键性能指标粒子数量10K 以下可用 Points JS 更新10K-100K 需用 Shader 驱动100K 需用 GPGPUCompute ShaderDraw CallPoints 对象只产生 1 个 Draw Call远优于独立 Mesh内存占用每个粒子约 28 bytesposition 12 color 12 size 410 万粒子约 2.8MB三、赛博朋克粒子系统的完整实现// cyberpunkParticles.ts — 赛博朋克风格粒子系统 // 设计意图实现高性能的赛博朋克视觉粒子效果 // 包含数据雨、全息碎片和霓虹光晕三种粒子层 import * as THREE from three; import { EffectComposer } from three/examples/jsm/postprocessing/EffectComposer; import { RenderPass } from three/examples/jsm/postprocessing/RenderPass; import { UnrealBloomPass } from three/examples/jsm/postprocessing/UnrealBloomPass; // ---- 自定义着色器 ---- const particleVertexShader uniform float uTime; uniform vec2 uMouse; uniform float uPixelRatio; attribute float aSize; attribute float aSpeed; attribute float aOffset; varying vec3 vColor; varying float vAlpha; // 简化的噪声函数用于有机运动 float hash(float n) { return fract(sin(n) * 43758.5453); } void main() { vec3 pos position; // 数据雨效果粒子沿 Y 轴下落循环重置 float fallSpeed aSpeed * 2.0; pos.y mod(pos.y - uTime * fallSpeed aOffset * 50.0, 100.0) - 50.0; // 水平微扰基于噪声的有机摆动 float noise hash(aOffset uTime * 0.1); pos.x sin(uTime * 0.5 aOffset * 6.28) * 0.3 * noise; // 鼠标排斥力粒子远离鼠标指针 vec2 toMouse pos.xz - uMouse; float mouseDist length(toMouse); if (mouseDist 5.0) { vec2 repel normalize(toMouse) * (5.0 - mouseDist) * 0.5; pos.xz repel; } // MVP 变换 vec4 mvPosition modelViewMatrix * vec4(pos, 1.0); gl_Position projectionMatrix * mvPosition; // 粒子大小近大远小 呼吸效果 float breathe 1.0 sin(uTime * 2.0 aOffset * 10.0) * 0.3; gl_PointSize aSize * uPixelRatio * breathe * (300.0 / -mvPosition.z); // 颜色赛博朋克色调青色 / 品红 / 电光蓝 float colorPhase aOffset * 3.0; vColor mix( vec3(0.0, 1.0, 0.88), // 青色 #00FFE0 vec3(1.0, 0.0, 0.88), // 品红 #FF00E0 sin(colorPhase) * 0.5 0.5 ); vColor mix(vColor, vec3(0.2, 0.4, 1.0), hash(aOffset 1.0) * 0.3); // 透明度底部淡出 闪烁 float fadeY smoothstep(-50.0, -30.0, pos.y); float flicker 0.7 0.3 * sin(uTime * 8.0 aOffset * 100.0); vAlpha fadeY * flicker; } ; const particleFragmentShader varying vec3 vColor; varying float vAlpha; void main() { // 圆形粒子丢弃正方形边角 float dist length(gl_PointCoord - vec2(0.5)); if (dist 0.5) discard; // 柔和边缘 float alpha vAlpha * smoothstep(0.5, 0.2, dist); // 中心高亮霓虹灯效果 float glow exp(-dist * 6.0) * 0.5; vec3 finalColor vColor vColor * glow; gl_FragColor vec4(finalColor, alpha); } ; // ---- 粒子系统类 ---- export class CyberpunkParticles { private scene: THREE.Scene; private camera: THREE.PerspectiveCamera; private renderer: THREE.WebGLRenderer; private composer: EffectComposer; private particles: THREE.Points; private clock: THREE.Clock; private mouse: THREE.Vector2; constructor(container: HTMLElement) { // 场景初始化 this.scene new THREE.Scene(); this.scene.fog new THREE.FogExp2(0x0a0a1a, 0.015); this.camera new THREE.PerspectiveCamera( 75, container.clientWidth / container.clientHeight, 0.1, 200 ); this.camera.position.set(0, 5, 30); this.renderer new THREE.WebGLRenderer({ antialias: true, alpha: true, }); this.renderer.setSize(container.clientWidth, container.clientHeight); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); container.appendChild(this.renderer.domElement); // 后处理Bloom 效果是赛博朋克视觉的灵魂 this.composer new EffectComposer(this.renderer); this.composer.addPass(new RenderPass(this.scene, this.camera)); const bloomPass new UnrealBloomPass( new THREE.Vector2(container.clientWidth, container.clientHeight), 1.5, // 强度霓虹灯的发光程度 0.4, // 半径 0.85 // 阈值只有亮度超过此值的像素才发光 ); this.composer.addPass(bloomPass); this.clock new THREE.Clock(); this.mouse new THREE.Vector2(0, 0); // 创建粒子 this.particles this.createParticles(50000); this.scene.add(this.particles); // 事件监听 window.addEventListener(mousemove, this.onMouseMove.bind(this)); window.addEventListener(resize, this.onResize.bind(this, container)); } private createParticles(count: number): THREE.Points { const geometry new THREE.BufferGeometry(); // 粒子属性数组 const positions new Float32Array(count * 3); const sizes new Float32Array(count); const speeds new Float32Array(count); const offsets new Float32Array(count); for (let i 0; i count; i) { // 位置分布在长方体空间中 positions[i * 3] (Math.random() - 0.5) * 80; // x positions[i * 3 1] (Math.random() - 0.5) * 100; // y positions[i * 3 2] (Math.random() - 0.5) * 40; // z // 大小大部分小粒子少量大粒子 sizes[i] Math.random() 0.95 ? Math.random() * 2 0.5 : Math.random() * 5 3; // 下落速度 speeds[i] Math.random() * 0.8 0.2; // 随机偏移用于噪声计算 offsets[i] Math.random(); } geometry.setAttribute(position, new THREE.BufferAttribute(positions, 3)); geometry.setAttribute(aSize, new THREE.BufferAttribute(sizes, 1)); geometry.setAttribute(aSpeed, new THREE.BufferAttribute(speeds, 1)); geometry.setAttribute(aOffset, new THREE.BufferAttribute(offsets, 1)); // 材质自定义着色器 const material new THREE.ShaderMaterial({ vertexShader: particleVertexShader, fragmentShader: particleFragmentShader, uniforms: { uTime: { value: 0 }, uMouse: { value: new THREE.Vector2(0, 0) }, uPixelRatio: { value: this.renderer.getPixelRatio() }, }, transparent: true, depthWrite: false, // 避免粒子间的深度冲突 blending: THREE.AdditiveBlending, // 叠加混合光效更自然 }); return new THREE.Points(geometry, material); } private onMouseMove(event: MouseEvent): void { // 将鼠标坐标映射到世界空间 this.mouse.x (event.clientX / window.innerWidth) * 2 - 1; this.mouse.y -(event.clientY / window.innerHeight) * 2 1; } private onResize(container: HTMLElement): void { const width container.clientWidth; const height container.clientHeight; this.camera.aspect width / height; this.camera.updateProjectionMatrix(); this.renderer.setSize(width, height); this.composer.setSize(width, height); } public animate(): void { requestAnimationFrame(this.animate.bind(this)); const elapsed this.clock.getElapsedTime(); // 更新 Uniform 变量 const material this.particles.material as THREE.ShaderMaterial; material.uniforms.uTime.value elapsed; material.uniforms.uMouse.value.set(this.mouse.x * 20, this.mouse.y * 20); // 相机缓慢旋转增强沉浸感 this.camera.position.x Math.sin(elapsed * 0.1) * 5; this.camera.lookAt(0, 0, 0); // 使用后处理渲染 this.composer.render(); } public dispose(): void { this.particles.geometry.dispose(); (this.particles.material as THREE.ShaderMaterial).dispose(); this.renderer.dispose(); } }四、粒子系统的 Trade-offs 与性能边界GPU 瓶颈与 Draw CallPoints 对象只产生 1 个 Draw Call但 Fragment Shader 的复杂度直接影响帧率。当粒子数量超过 50K 且每个粒子的 Fragment 计算包含多次纹理采样时GPU 填充率成为瓶颈。优化方向是简化 Fragment Shader减少exp、sin等函数调用或使用纹理图集Texture Atlas替代程序化着色。Bloom 后处理的性能开销UnrealBloomPass 需要多次降采样和高斯模糊在 1080p 分辨率下约占 3-5ms 的渲染时间。对于移动端Bloom 的开销可能不可接受。替代方案是使用 CSSfilter: blur()mix-blend-mode: screen模拟发光效果性能开销更低但效果受限。粒子生命周期的内存管理Three.js 的 BufferGeometry 在创建时分配固定大小的缓冲区无法动态扩展。如果需要频繁增删粒子必须预分配最大数量的缓冲区通过drawRange控制实际渲染数量避免反复创建和销毁 Geometry 对象。WebGL 兼容性自定义 Shader 依赖 WebGL 2.0 的 GLSL 300 es 特性。在部分旧设备上可能不支持需要提供降级方案使用内置 PointsMaterial 纹理贴图。五、总结Three.js 粒子系统通过 Shader 驱动的批量渲染和后处理叠加能够高效实现赛博朋克风格的视觉氛围。核心原则是计算在 GPU数据传一次——通过 Uniform 变量驱动 Vertex Shader 中的运动计算避免 CPU 端逐粒子更新。但 Bloom 后处理的性能开销、粒子数量的 GPU 填充率限制、固定缓冲区的内存管理约束和 WebGL 兼容性是需要权衡的因素。在实际落地中建议桌面端使用完整 Shader Bloom 方案移动端降级为简化 Shader CSS 模糊在视觉效果和性能之间取得平衡。