Three.js 赛博朋克风格 UI:霓虹光效与粒子系统的 WebGL 渲染实战
一、赛博朋克 UI 的渲染挑战:从视觉概念到 WebGL 像素
赛博朋克风格的 Web 界面有三个标志性视觉元素:霓虹光效(Neon Glow)、全息粒子场(Holographic Particle Field)和故障艺术(Glitch Art)。这些效果在 2D CSS 中可以通过 box-shadow、filter 和 animation 近似模拟,但当场景复杂度上升——数百个发光元素、数千个粒子、实时交互响应——CSS 方案的性能会急剧恶化,帧率跌破 30fps。
根本原因在于 CSS 的渲染管线无法利用 GPU 的并行计算能力。box-shadow 的模糊计算在 CPU 上执行,每个阴影元素独立计算,无法批量处理。而 Three.js 基于 WebGL 的渲染管线天然运行在 GPU 上,通过着色器(Shader)程序将光效和粒子计算并行化,可以在 60fps 下渲染数万个粒子。本文将深入探讨如何用 Three.js 构建生产级的赛博朋克风格 UI。
二、赛博朋克渲染管线:后处理与粒子系统的协同架构
赛博朋克风格的核心渲染管线由三层组成:场景层(3D 几何体与粒子)、后处理层(Bloom 辉光、色差、故障效果)和交互层(鼠标追踪、滚动驱动)。三层通过 EffectComposer 串联,每帧依次执行。
flowchart LR subgraph 场景层 Scene[Three.js Scene] --> Geometry[霓虹几何体] Scene --> Particles[粒子系统] Scene --> Grid[无限网格地面] end subgraph 渲染通道 Geometry --> RenderPass[基础渲染通道] Particles --> RenderPass Grid --> RenderPass RenderPass --> UnrealBloom[Bloom 辉光通道] UnrealBloom --> GlitchPass[故障效果通道] GlitchPass --> Chromatic[色差通道] Chromatic --> Output[最终输出] end subgraph 交互层 Mouse[鼠标位置] --> |uniform| Particles Mouse --> |uniform| Geometry Scroll[滚动偏移] --> |transform| Grid end style 场景层 fill:#0a0a23,stroke:#00ffff,color:#eee style 渲染通道 fill:#1a0a3e,stroke:#ff00ff,color:#eee style 交互层 fill:#0d1b2a,stroke:#00ff88,color:#eee上图展示了渲染管线的三层架构。关键设计点在于 Bloom 辉光通道的参数调优——赛博朋克风格的霓虹光效需要高阈值(threshold)配合高强度(strength),只让最亮的区域发光,避免整个画面泛白。粒子系统则通过自定义 ShaderMaterial 实现,每个粒子的颜色和大小根据距摄像机的距离动态变化,模拟全息投影的远近衰减效果。
三、赛博朋克 UI 的 Three.js 工程实现
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'; import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass'; import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass'; // 赛博朋克场景管理器 // 封装 Three.js 的初始化、渲染循环和后处理管线 class CyberpunkScene { private scene: THREE.Scene; private camera: THREE.PerspectiveCamera; private renderer: THREE.WebGLRenderer; private composer: EffectComposer; private particles: THREE.Points; private neonObjects: THREE.Group; private mouse: THREE.Vector2; private clock: THREE.Clock; constructor(container: HTMLElement) { // 初始化渲染器——开启抗锯齿和 HDR 输出 // HDR 是 Bloom 辉光效果的必要前提,否则光效会丢失亮度信息 this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: 'high-performance', }); this.renderer.setSize(container.clientWidth, container.clientHeight); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); this.renderer.toneMapping = THREE.ACESFilmicToneMapping; this.renderer.toneMappingExposure = 1.2; container.appendChild(this.renderer.domElement); // 场景与相机 this.scene = new THREE.Scene(); this.scene.fog = new THREE.FogExp2(0x0a0a23, 0.015); // 指数雾——模拟赛博朋克的城市雾霾 this.camera = new THREE.PerspectiveCamera( 75, container.clientWidth / container.clientHeight, 0.1, 1000 ); this.camera.position.set(0, 2, 8); this.mouse = new THREE.Vector2(0, 0); this.clock = new THREE.Clock(); // 构建场景元素 this.particles = this.createParticleSystem(5000); this.neonObjects = this.createNeonGeometry(); this.scene.add(this.particles, this.neonObjects, this.createInfiniteGrid()); // 构建后处理管线 this.composer = this.createPostProcessing(); // 事件监听 window.addEventListener('mousemove', this.onMouseMove); window.addEventListener('resize', this.onResize); // 启动渲染循环 this.animate(); } /** * 创建粒子系统——赛博朋克的全息粒子场 * 使用自定义 ShaderMaterial 实现粒子的颜色渐变和大小衰减 * 每个粒子有独立的速度向量,在 animate 中更新位置 */ private createParticleSystem(count: number): THREE.Points { const geometry = new THREE.BufferGeometry(); // 粒子位置——随机分布在空间中 const positions = new Float32Array(count * 3); // 粒子颜色——从青色到品红的渐变,赛博朋克标志色 const colors = new Float32Array(count * 3); // 粒子大小——根据距摄像机距离衰减 const sizes = new Float32Array(count); // 粒子速度——用于动画更新 const velocities = new Float32Array(count * 3); for (let i = 0; i < count; i++) { const i3 = i * 3; // 位置:在 20x20x20 的空间内随机分布 positions[i3] = (Math.random() - 0.5) * 20; positions[i3 + 1] = (Math.random() - 0.5) * 20; positions[i3 + 2] = (Math.random() - 0.5) * 20; // 颜色:在青色(#00ffff)和品红(#ff00ff)之间随机插值 const t = Math.random(); colors[i3] = t; // R: 0 → 1 colors[i3 + 1] = 1 - t; // G: 1 → 0 colors[i3 + 2] = 1; // B: 固定 1 sizes[i] = Math.random() * 3 + 0.5; // 速度:缓慢漂浮 velocities[i3] = (Math.random() - 0.5) * 0.01; velocities[i3 + 1] = (Math.random() - 0.5) * 0.01; velocities[i3 + 2] = (Math.random() - 0.5) * 0.01; } geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); // 自定义着色器——实现粒子大小随距离衰减和颜色脉冲 const material = new THREE.ShaderMaterial({ uniforms: { uTime: { value: 0 }, uMouse: { value: new THREE.Vector2(0, 0) }, uPixelRatio: { value: this.renderer.getPixelRatio() }, }, vertexShader: ` attribute float size; varying vec3 vColor; uniform float uTime; uniform vec2 uMouse; uniform float uPixelRatio; void main() { vColor = color; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); // 粒子大小随距离衰减——近大远小 gl_PointSize = size * uPixelRatio * (200.0 / -mvPosition.z); // 鼠标靠近时粒子膨胀——交互反馈 float distToMouse = length(position.xy - uMouse * 5.0); float mouseInfluence = smoothstep(3.0, 0.0, distToMouse); gl_PointSize *= (1.0 + mouseInfluence * 2.0); gl_Position = projectionMatrix * mvPosition; } `, fragmentShader: ` varying vec3 vColor; uniform float uTime; void main() { // 圆形粒子——丢弃圆外的像素 float dist = length(gl_PointCoord - vec2(0.5)); if (dist > 0.5) discard; // 中心亮、边缘暗——模拟发光效果 float glow = 1.0 - dist * 2.0; glow = pow(glow, 1.5); // 颜色脉冲——随时间微弱闪烁 float pulse = 0.8 + 0.2 * sin(uTime * 2.0 + vColor.r * 10.0); gl_FragColor = vec4(vColor * glow * pulse, glow * 0.8); } `, transparent: true, vertexColors: true, blending: THREE.AdditiveBlending, // 加法混合——粒子叠加更亮 depthWrite: false, // 不写深度——避免粒子互相遮挡 }); // 将速度数据存储在 userData 中,供动画循环使用 const points = new THREE.Points(geometry, material); points.userData.velocities = velocities; return points; } /** * 创建霓虹几何体——赛博朋克的标志性发光线框 * 使用 EdgesGeometry 只渲染边缘线,配合高亮度颜色触发 Bloom */ private createNeonGeometry(): THREE.Group { const group = new THREE.Group(); // 霓虹色板——青色、品红、黄色三原色 const neonColors = [0x00ffff, 0xff00ff, 0xffff00]; // 创建多个旋转的线框几何体 const geometries = [ new THREE.IcosahedronGeometry(1.5, 1), new THREE.OctahedronGeometry(1.2, 0), new THREE.TorusGeometry(1.0, 0.3, 8, 32), ]; geometries.forEach((geo, index) => { // EdgesGeometry 只提取边缘线——比 WireframeGeometry 更干净 const edges = new THREE.EdgesGeometry(geo); const lineMaterial = new THREE.LineBasicMaterial({ color: neonColors[index], // 高亮度是触发 Bloom 辉光的关键 // Three.js 的 Bloom 基于亮度阈值,超过阈值的像素才会发光 }); const line = new THREE.LineSegments(edges, lineMaterial); line.position.set( (index - 1) * 3.5, Math.sin(index) * 0.5, 0 ); group.add(line); }); return group; } /** * 创建无限网格地面——赛博朋克的透视参考线 * 使用 ShaderMaterial 宓现网格线的渐隐效果 */ private createInfiniteGrid(): THREE.Mesh { const gridMaterial = new THREE.ShaderMaterial({ uniforms: { uColor: { value: new THREE.Color(0x00ffff) }, uFadeDistance: { value: 30.0 }, }, vertexShader: ` varying vec3 vWorldPos; void main() { vec4 worldPos = modelMatrix * vec4(position, 1.0); vWorldPos = worldPos.xyz; gl_Position = projectionMatrix * viewMatrix * worldPos; } `, fragmentShader: ` uniform vec3 uColor; uniform float uFadeDistance; varying vec3 vWorldPos; void main() { // 网格线——基于世界坐标的整数边界 vec2 grid = abs(fract(vWorldPos.xz - 0.5) - 0.5); float line = min(grid.x, grid.y); float gridAlpha = 1.0 - smoothstep(0.0, 0.05, line); // 距离衰减——远处网格线逐渐消失 float dist = length(vWorldPos.xz); float fade = 1.0 - smoothstep(0.0, uFadeDistance, dist); gl_FragColor = vec4(uColor, gridAlpha * fade * 0.3); } `, transparent: true, side: THREE.DoubleSide, depthWrite: false, }); const gridGeometry = new THREE.PlaneGeometry(100, 100, 1, 1); const gridMesh = new THREE.Mesh(gridGeometry, gridMaterial); gridMesh.rotation.x = -Math.PI / 2; gridMesh.position.y = -2; return gridMesh; } /** * 创建后处理管线——赛博朋克视觉效果的最终合成 * Bloom 辉光是核心,Glitch 和色差是锦上添花 */ private createPostProcessing(): EffectComposer { const composer = new EffectComposer(this.renderer); // 基础渲染通道 const renderPass = new RenderPass(this.scene, this.camera); composer.addPass(renderPass); // Bloom 辉光——赛博朋克的灵魂效果 // threshold: 只有亮度超过此值的像素才发光,避免全屏泛白 // strength: 辉光强度,越高越梦幻 // radius: 辉光扩散半径 const bloomPass = new UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, // strength 0.4, // radius 0.2 // threshold——低阈值让更多元素发光 ); composer.addPass(bloomPass); // 故障效果——赛博朋克的数字失真感 // 不常触发,只在特定交互时激活 const glitchPass = new GlitchPass(); glitchPass.goWild = false; // 默认关闭,交互时临时开启 composer.addPass(glitchPass); // 自定义色差着色器——RGB 通道偏移 const chromaticAberrationShader = { uniforms: { tDiffuse: { value: null }, uOffset: { value: 0.002 }, // 色差偏移量 }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform sampler2D tDiffuse; uniform float uOffset; varying vec2 vUv; void main() { // RGB 三个通道分别偏移——模拟镜头色差 vec2 dir = vUv - vec2(0.5); float dist = length(dir); vec2 offset = dir * dist * uOffset; float r = texture2D(tDiffuse, vUv + offset).r; float g = texture2D(tDiffuse, vUv).g; float b = texture2D(tDiffuse, vUv - offset).b; gl_FragColor = vec4(r, g, b, 1.0); } `, }; const chromaticPass = new ShaderPass(chromaticAberrationShader); composer.addPass(chromaticPass); return composer; } // 渲染循环 private animate = (): void => { requestAnimationFrame(this.animate); const elapsed = this.clock.getElapsedTime(); // 更新粒子位置 this.updateParticles(elapsed); // 旋转霓虹几何体 this.neonObjects.children.forEach((obj, i) => { obj.rotation.x = elapsed * 0.3 * (i + 1) * 0.5; obj.rotation.y = elapsed * 0.2 * (i + 1) * 0.5; }); // 更新着色器 uniform const particleMaterial = this.particles.material as THREE.ShaderMaterial; particleMaterial.uniforms.uTime.value = elapsed; particleMaterial.uniforms.uMouse.value.copy(this.mouse); // 渲染 this.composer.render(); }; private updateParticles(elapsed: number): void { const positions = this.particles.geometry.attributes.position.array as Float32Array; const velocities = this.particles.userData.velocities as Float32Array; for (let i = 0; i < positions.length; i += 3) { positions[i] += velocities[i]; positions[i + 1] += velocities[i + 1]; positions[i + 2] += velocities[i + 2]; // 边界回弹——粒子超出范围后反向运动 for (let j = 0; j < 3; j++) { if (Math.abs(positions[i + j]) > 10) { velocities[i + j] *= -1; } } } this.particles.geometry.attributes.position.needsUpdate = true; } 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 = (): void => { const width = window.innerWidth; const height = window.innerHeight; this.camera.aspect = width / height; this.camera.updateProjectionMatrix(); this.renderer.setSize(width, height); this.composer.setSize(width, height); }; }四、WebGL 渲染的性能边界与设备兼容性
移动端 GPU 的算力瓶颈。上述场景在桌面端 GPU(如 RTX 3060)上可以稳定 60fps,但在中端移动设备上帧率可能降至 20-30fps。主要瓶颈在于粒子系统的 Fragment Shader——每个粒子需要计算发光衰减和颜色脉冲,5000 个粒子意味着每帧执行 5000 次片段着色器调用。移动端优化策略包括:降低粒子数至 1000-2000、使用 InstancedBufferGeometry 替代 Points、关闭 AdditiveBlending 改用普通透明混合。
Bloom 辉光的性能开销。UnrealBloomPass 需要对渲染结果做多次降采样和高斯模糊,每增加一级降采样就多一次全屏 Pass。在 1080p 分辨率下,5 级 Bloom 大约增加 3-4ms 的渲染时间。对于低端设备,可以减少 Bloom 级数至 3 级,或降低 Bloom Pass 的分辨率至屏幕的 1/2。
Shader 编译的冷启动延迟。自定义 ShaderMaterial 在首次使用时需要编译 GLSL 着色器,编译时间取决于着色器复杂度和 GPU 驱动。在低端 Android 设备上,冷启动编译可能需要 500ms-2s,期间页面会卡住。解决方案是在页面加载后立即渲染一帧不可见的场景,触发着色器预编译。
WebGL 上下文丢失。移动浏览器在内存压力下可能回收 WebGL 上下文,导致整个 3D 场景崩溃。Three.js 提供了renderer.info监控机制,但上下文恢复需要重新创建所有 GPU 资源。生产环境中必须监听webglcontextlost事件,实现优雅降级——3D 场景切换为 CSS 静态背景。
五、总结
Three.js 为赛博朋克风格 UI 提供了远超 CSS 的渲染能力和性能上限。通过自定义 ShaderMaterial 实现粒子系统的颜色脉冲和大小衰减,通过 UnrealBloomPass 实现霓虹辉光效果,通过色差着色器模拟镜头失真,三层后处理管线协同工作构建出完整的赛博朋克视觉体验。落地路线建议:第一步,使用 EffectComposer 搭建基础后处理管线,优先调通 Bloom 辉光参数;第二步,将粒子系统和霓虹几何体封装为独立的 React 组件,通过 R3F(React Three Fiber)集成到现有页面中;第三步,针对移动端实现 LOD(Level of Detail)策略——根据设备 GPU 等级动态调整粒子数量和 Bloom 级数。始终需要在视觉冲击力与渲染性能之间找到平衡点——60fps 的流畅体验比华丽的静态截图更有价值。