Three.js 3D 渲染与赛博朋克风格 UI 实现:从着色器到霓虹矩阵
Three.js 3D 渲染与赛博朋克风格 UI 实现:从着色器到霓虹矩阵
一、Web 3D 的"赛博觉醒":为什么前端需要第三维度
Web 页面长期被困在二维平面里。CSS 动画再炫,也只是平面的位移和变换。当你的 DApp 需要展示链上数据的三维可视化,当你的产品需要赛博朋克风格的沉浸式体验,当你的用户期望在浏览器中看到电影级的视觉效果——Three.js 是从平面跃入空间的桥梁。
但 3D 渲染不是"加个 3D 模型"那么简单。着色器编程、光照模型、后处理管线、性能优化——每一项都是独立的学问。赛博朋克风格更是一个特殊的挑战:它不是简单的"加个霓虹灯",而是需要精心设计色彩体系、粒子系统、故障效果和氛围渲染,才能营造出那种"高科技、低生活"的视觉张力。
二、Three.js 渲染管线与赛博朋克视觉体系
2.1 渲染管线架构
Three.js 的渲染管线从场景构建到像素输出,每一步都可以定制。理解这个管线,是实现自定义视觉效果的基础。
graph TD A[Scene 场景图] --> B[Geometry 几何体] A --> C[Material 材质] A --> D[Light 光照] B --> E[Vertex Shader 顶点着色器] C --> E D --> E E --> F[光栅化] F --> G[Fragment Shader 片元着色器] G --> H[帧缓冲] H --> I[后处理管线] I --> J[Bloom 辉光] I --> K[Glitch 故障] I --> L[Film Grain 胶片噪点] J --> M[最终输出] K --> M L --> M2.2 赛博朋克色彩体系
赛博朋克的视觉核心是高对比度 + 霓虹色。典型色彩方案:
- 主色调:深蓝黑(#0a0a1a)作为背景,营造暗夜氛围
- 霓虹色:青色(#00fff2)、品红(#ff00ff)、电紫(#8b5cf6)
- 强调色:热橙(#ff6b35)用于警告和交互元素
- 辅助色:冷灰(#2a2a3a)用于面板和边框
这些颜色不是随意选择的。青色和品红的互补关系创造视觉张力,深色背景让霓虹色"发光",热橙的暖色调在冷色系中形成焦点。
2.3 自定义着色器基础
赛博朋克效果的核心是自定义着色器。Three.js 的 ShaderMaterial 允许你完全控制顶点和片元处理。
三、赛博朋克 UI 的完整实现
3.1 霓虹网格地面
// components/cyber-grid.ts import * as THREE from "three"; /** * 赛博朋克风格的无限网格地面 * 为什么用着色器而非几何体? * 几何体网格的线段数量随距离指数增长, * 着色器只需一个平面,通过数学计算生成网格线 */ export function createCyberGrid(): THREE.Mesh { const geometry = new THREE.PlaneGeometry(200, 200, 1, 1); geometry.rotateX(-Math.PI / 2); const material = new THREE.ShaderMaterial({ uniforms: { uTime: { value: 0 }, uColor: { value: new THREE.Color("#00fff2") }, uFadeColor: { value: new THREE.Color("#0a0a1a") }, }, vertexShader: ` varying vec2 vUv; varying vec3 vWorldPos; void main() { vUv = uv; // 计算世界坐标,用于网格线计算 vec4 worldPos = modelMatrix * vec4(position, 1.0); vWorldPos = worldPos.xyz; gl_Position = projectionMatrix * viewMatrix * worldPos; } `, fragmentShader: ` uniform float uTime; uniform vec3 uColor; uniform vec3 uFadeColor; varying vec2 vUv; varying vec3 vWorldPos; void main() { // 网格线:基于世界坐标的周期函数 // 为什么用 fmod 而非纹理? // 数学计算无限精度,不会出现纹理模糊 float gridX = abs(fract(vWorldPos.x * 0.1) - 0.5); float gridZ = abs(fract(vWorldPos.z * 0.1) - 0.5); // 线宽:距离越远线越细 float lineWidth = 0.02; float lineX = smoothstep(lineWidth, 0.0, gridX); float lineZ = smoothstep(lineWidth, 0.0, gridZ); float grid = max(lineX, lineZ); // 距离衰减:远处网格线逐渐消失 float dist = length(vWorldPos.xz); float fade = 1.0 - smoothstep(10.0, 80.0, dist); // 脉冲动画:网格线周期性闪烁 float pulse = sin(dist * 0.3 - uTime * 2.0) * 0.3 + 0.7; vec3 color = mix(uFadeColor, uColor, grid * fade * pulse); float alpha = grid * fade * 0.8; gl_FragColor = vec4(color, alpha); } `, transparent: true, side: THREE.DoubleSide, }); return new THREE.Mesh(geometry, material); }3.2 全息粒子系统
// components/hologram-particles.ts import * as THREE from "three"; /** * 全息风格粒子系统 * 为什么用 Points 而非独立 Mesh? * 数千个粒子用独立 Mesh 性能不可接受, * Points 将所有粒子合并为一次绘制调用 */ export function createHologramParticles(count: number = 2000): THREE.Points { const positions = new Float32Array(count * 3); const sizes = new Float32Array(count); const phases = new Float32Array(count); // 每个粒子的相位偏移 for (let i = 0; i < count; i++) { // 在圆柱体内随机分布——赛博朋克的城市天际线感 const angle = Math.random() * Math.PI * 2; const radius = 5 + Math.random() * 40; const height = Math.random() * 30 - 5; positions[i * 3] = Math.cos(angle) * radius; positions[i * 3 + 1] = height; positions[i * 3 + 2] = Math.sin(angle) * radius; sizes[i] = Math.random() * 3 + 0.5; phases[i] = Math.random() * Math.PI * 2; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); geometry.setAttribute("aSize", new THREE.BufferAttribute(sizes, 1)); geometry.setAttribute("aPhase", new THREE.BufferAttribute(phases, 1)); const material = new THREE.ShaderMaterial({ uniforms: { uTime: { value: 0 }, uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) }, }, vertexShader: ` attribute float aSize; attribute float aPhase; uniform float uTime; uniform float uPixelRatio; varying float vAlpha; void main() { vec3 pos = position; // 粒子缓慢上升,到达顶部后重置 // 为什么用 mod 而非 if? // GPU 着色器中分支语句性能差, // 数学运算更高效 pos.y = mod(pos.y + uTime * 0.5, 30.0) - 5.0; // 呼吸效果:粒子大小周期性变化 float breathe = sin(uTime * 2.0 + aPhase) * 0.3 + 1.0; vec4 mvPos = modelViewMatrix * vec4(pos, 1.0); gl_PointSize = aSize * breathe * uPixelRatio * (80.0 / -mvPos.z); gl_Position = projectionMatrix * mvPos; // 透明度随高度变化 vAlpha = smoothstep(-5.0, 5.0, pos.y) * smoothstep(30.0, 20.0, pos.y); } `, fragmentShader: ` varying float vAlpha; void main() { // 圆形粒子:距离中心越远越透明 float dist = length(gl_PointCoord - vec2(0.5)); if (dist > 0.5) discard; float alpha = smoothstep(0.5, 0.1, dist) * vAlpha * 0.6; // 青色全息粒子 vec3 color = vec3(0.0, 1.0, 0.95); gl_FragColor = vec4(color, alpha); } `, transparent: true, depthWrite: false, // 粒子不写入深度缓冲,避免遮挡问题 blending: THREE.AdditiveBlending, // 叠加混合,让粒子发光 }); return new THREE.Points(geometry, material); }3.3 故障效果后处理
// postprocessing/glitch-effect.ts import { Effect } from "postprocessing"; /** * 自定义赛博朋克故障效果 * 为什么用后处理而非 CSS? * 后处理在 GPU 上执行,性能远优于 CSS filter, * 且能与 3D 场景深度信息结合 */ export const glitchFragmentShader = /* glsl */ ` uniform float uTime; uniform float uIntensity; uniform float uGlitchFrequency; // 伪随机函数——为什么不用 JS Math.random? // 着色器在 GPU 上执行,无法调用 JS 函数 float random(vec2 st) { return fract(sin(dot(st, vec2(12.9898, 78.233))) * 43758.5453); } void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { // 触发故障:基于时间的随机脉冲 float glitchTrigger = step( 0.95, random(vec2(floor(uTime * uGlitchFrequency), 1.0)) ); // 水平位移:RGB 通道分别偏移 float offset = glitchTrigger * uIntensity * 0.05; float r = texture2D(inputBuffer, uv + vec2(offset, 0.0)).r; float g = texture2D(inputBuffer, uv).g; float b = texture2D(inputBuffer, uv - vec2(offset, 0.0)).b; vec3 color = vec3(r, g, b); // 扫描线效果 float scanline = sin(uv.y * 800.0) * 0.04; color -= scanline; // 随机色块——模拟数据损坏 float blockNoise = glitchTrigger * step( 0.5, random(vec2(floor(uv.y * 50.0), floor(uTime * 10.0))) ); color = mix(color, vec3(1.0, 0.0, 0.95), blockNoise * 0.3); outputColor = vec4(color, inputColor.a); } `;3.4 场景组装与动画循环
// scene/cyber-scene.ts import * as THREE from "three"; import { EffectComposer, BloomEffect, RenderPass, EffectPass } from "postprocessing"; import { createCyberGrid } from "../components/cyber-grid"; import { createHologramParticles } from "../components/hologram-particles"; export class CyberScene { private renderer: THREE.WebGLRenderer; private scene: THREE.Scene; private camera: THREE.PerspectiveCamera; private composer: EffectComposer; private clock: THREE.Clock; // 场景元素引用 private grid: THREE.Mesh; private particles: THREE.Points; constructor(container: HTMLElement) { // 渲染器 this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, }); this.renderer.setSize(container.clientWidth, container.clientHeight); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); this.renderer.toneMapping = THREE.ACESFilmicToneMapping; container.appendChild(this.renderer.domElement); // 场景 this.scene = new THREE.Scene(); this.scene.fog = new THREE.FogExp2("#0a0a1a", 0.015); // 为什么用雾?赛博朋克场景需要深度感, // 雾效让远处物体逐渐消失,增强空间纵深 // 相机 this.camera = new THREE.PerspectiveCamera( 60, container.clientWidth / container.clientHeight, 0.1, 200 ); this.camera.position.set(0, 8, 25); this.camera.lookAt(0, 3, 0); // 创建场景元素 this.grid = createCyberGrid(); this.scene.add(this.grid); this.particles = createHologramParticles(3000); this.scene.add(this.particles); // 环境光——为什么用低强度? // 赛博朋克场景的光源应该是局部的霓虹灯, // 全局环境光只需提供最低可见度 const ambientLight = new THREE.AmbientLight("#1a1a2e", 0.3); this.scene.add(ambientLight); // 点光源模拟霓虹灯 const neonLight1 = new THREE.PointLight("#00fff2", 2, 50); neonLight1.position.set(10, 10, 10); this.scene.add(neonLight1); const neonLight2 = new THREE.PointLight("#ff00ff", 1.5, 40); neonLight2.position.set(-15, 8, -5); this.scene.add(neonLight2); // 后处理管线 this.composer = new EffectComposer(this.renderer); this.composer.addPass(new RenderPass(this.scene, this.camera)); // 辉光效果——赛博朋克的灵魂 // 为什么辉光如此重要?霓虹灯的视觉特征就是光晕, // 没有 Bloom 效果,霓虹色和平面色没有区别 const bloom = new BloomEffect({ intensity: 1.5, luminanceThreshold: 0.2, luminanceSmoothing: 0.9, mipmapBlur: true, }); this.composer.addPass(new EffectPass(this.camera, bloom)); this.clock = new THREE.Clock(); // 响应窗口大小变化 window.addEventListener("resize", this.onResize); } private onResize = () => { 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); }; public animate = () => { requestAnimationFrame(this.animate); const elapsed = this.clock.getElapsedTime(); // 更新着色器时间 uniform const gridMat = this.grid.material as THREE.ShaderMaterial; gridMat.uniforms.uTime.value = elapsed; const particleMat = this.particles.material as THREE.ShaderMaterial; particleMat.uniforms.uTime.value = elapsed; // 相机缓慢旋转——为什么不用鼠标控制? // 展示场景用自动旋转更稳定, // 交互场景可替换为 OrbitControls this.camera.position.x = Math.sin(elapsed * 0.1) * 25; this.camera.position.z = Math.cos(elapsed * 0.1) * 25; this.camera.lookAt(0, 3, 0); this.composer.render(); }; public dispose() { window.removeEventListener("resize", this.onResize); this.renderer.dispose(); } }3.5 React 集成
// components/CyberBackground.tsx "use client"; import { useEffect, useRef } from "react"; import { CyberScene } from "@/scene/cyber-scene"; export function CyberBackground() { const containerRef = useRef<HTMLDivElement>(null); const sceneRef = useRef<CyberScene | null>(null); useEffect(() => { if (!containerRef.current) return; // 创建场景 sceneRef.current = new CyberScene(containerRef.current); sceneRef.current.animate(); // 清理——为什么需要清理? // React 严格模式下 useEffect 会执行两次, // 不清理会导致 WebGL 上下文泄漏 return () => { sceneRef.current?.dispose(); sceneRef.current = null; }; }, []); return ( <div ref={containerRef} className="fixed inset-0 -z-10" style={{ background: "#0a0a1a" }} /> ); }四、架构权衡:3D 渲染的代价
4.1 视觉冲击 vs 性能开销
赛博朋克风格依赖大量后处理效果(Bloom、Glitch、粒子),这些效果在低端设备上可能导致帧率下降。建议根据设备性能动态调整:高端设备启用全部效果,低端设备关闭 Bloom 和粒子,只保留基础网格。
4.2 自定义着色器 vs 内置材质
ShaderMaterial 提供完全控制,但维护成本高——着色器代码不可调试,跨平台兼容性需要手动测试。MeshStandardMaterial + 后处理在大多数场景下够用,只有需要特殊效果(如全息投影、数据流)时才用自定义着色器。
4.3 实时渲染 vs 预渲染
背景动画可以预渲染为视频,用 CSS 播放,性能远优于实时渲染。但预渲染失去了交互性——鼠标移动无法影响场景。对于纯装饰性背景,预渲染是更务实的选择;对于交互式 3D 产品,实时渲染不可替代。
4.4 Bundle 体积
Three.js 完整包约 600KB(gzip 后约 150KB),加上后处理库和着色器代码,3D 场景的 JS 体积可能超过 300KB。对于内容型网站,这个体积不可接受。建议用动态 import 按需加载 3D 模块,首屏不阻塞。
五、总结
Three.js 赛博朋克 UI 的实现,本质上是"用代码绘制氛围"。网格地面营造空间感,粒子系统制造生命感,故障效果注入不安感,辉光后处理赋予霓虹灵魂。每一个视觉元素都在讲述同一个故事:高科技与低生活的碰撞。
但 3D 渲染的每一帧都是性能的消耗。自定义着色器、后处理管线、粒子系统——这些效果的叠加会迅速耗尽 GPU 资源。在赛博朋克的视觉追求和用户设备的现实限制之间,找到平衡点才是工程能力的体现。
在赛博空间的前端战场,Three.js 是你的画笔,着色器是你的颜料。但记住——最好的视觉效果,是让用户沉浸其中而忘记技术存在的那种。
