我用 Three.js 给博客首页造了一个赛博朋克房间
起因
写博客十年了,首页一直是千篇一律的静态 Banner + 文章列表。说实话看腻了。
我一直想搞点不一样的——不是换个背景图换个配色那种不一样,而是整个首页的第一印象就让人「卧槽」的那种。
所以我把首页 Banner 砸了,用 Three.js 造了一个赛博朋克风格的 3D 小公寓。
想法
核心概念很简单:赛博朋克废土中,一个被你不断修复、扩建的小站点。
房间不是冷冰冰的展示台,是有人住着的——桌上有咖啡杯,椅子上搭着外套,地上散落着线缆,角落里趴着一只机械狗。这其实就是我写博客的状态:一堆乱七八糟的东西堆在一起,但就是能产出东西。
远期规划还挺疯的:
- 合集 = 建筑群
- 文章 = 可交互的全息面板
- 标签 = 区域标识灯
- 博客成长 = 世界扩张
但现阶段只做第一步:3D Banner。
技术栈
选型没怎么犹豫:
- React Three Fiber (R3F) — React 生态的 3D 渲染器,跟 Next.js 天然集成
- @react-three/drei — 常用 3D 组件库(OrbitControls、Environment 等)
- @react-three/postprocessing — 后处理效果(Bloom 霓虹发光 + Vignette 暗角)
- Zustand — 场景参数状态管理
- leva — 开发调试面板(生产环境完全不渲染)
没自己建 3D 模型,所有东西都是代码搓出来的基础几何体或者程序化纹理。
房间长什么样
整个场景是一个赛博朋克小公寓,广角展示完整空间:
左侧睡眠区:一张乱糟糟的床,墙上贴着粉色发光海报,床头柜上有一个全息时钟。
中央工作区:工业风桌子,RGB 发光键盘,三台显示器分别显示代码界面、地图监控和系统状态。
右侧存储区:六层金属书架,摆着发光的数据核心和存储模块;旁边是一台 1.8 米高的服务器机柜,16 个 LED 指示灯各自独立闪烁。
后墙落地窗:4.5×3.5 米的大窗,窗外是程序化生成的赛博朋克城市天际线——85 栋建筑剪影、随机闪烁的霓虹窗灯、飞行载具光带。雨滴顺着玻璃往下流。
天花板:暴露的管道系统(8 根横管 + 2 条电缆桥架),嵌着三条霓虹灯带(青色、紫色、粉红)。
生活细节:桌下散落的线缆(TubeGeometry 弯曲线缆)、咖啡杯、椅子上的外套、十二面体生物发光植物、角落里的机械狗。
窗户上方还挂了一个霓虹灯牌——"小破站"。
几个我觉得有意思的技术点
程序化纹理
所有纹理都是 Canvas 2D 画的,没用贴图:
城市天际线(2048×1024):85 栋随机高度的建筑剪影,随机亮灭的霓虹窗灯,广告牌发光,飞行载具光带,多层雾气。每次生成都不一样。
木地板(512×512):木板纹理、缝隙、水渍、磨损痕迹。
混凝土墙(256×256):噪点 + 水渍。
全部通过 useMemo 缓存,只创建一次。
雨滴粒子系统
300 个粒子用 BufferGeometry + Points 实现。其中 20% 是"雨痕"——速度更快、尺寸更大、横向漂移更少,模拟雨水沿玻璃流下的效果。
用 AdditiveBlending + depthWrite: false 避免深度冲突,雨滴自然叠在窗后城市上。
灯光系统
27 个独立参数通过 Zustand 管理:
- 1 个 SpotLight 从窗外射入(模拟城市光照)
- 8 个 PointLight(显示器冷蓝光、服务器紫光、霓虹招牌等)
- 所有灯光都有独立的呼吸动画,频率各不相同
- 窗外主光是双频叠加(0.3Hz + 0.7Hz),模拟远处霓虹闪烁
动画
全部通过 useFrame 驱动:
- 服务器 LED:16 个灯各自随机频率闪烁
- 显示器 emissiveIntensity 微呼吸
- 全息时钟脉冲
- 霓虹海报/灯牌呼吸发光
- 窗户城市纹理 emissiveIntensity 呼吸
每个动画都很轻量,没有重型物理模拟。
性能策略
这块花了不少心思:
- WebGL 检测 + 降级:检测 GPU 能力,低端设备自动回退静态 Banner
- dpr 限制在 1~1.5:不做无意义的超采样
- 生产/开发严格隔离:生产环境直接读常量,不订阅 Zustand store,不渲染 DevControls / OrbitControls / Grid
- Zustand 细粒度 selector:开发环境每个组件只订阅自己需要的参数
- leva 调试面板:独立 React Root 挂载,跟主应用完全解耦,提供 8 个预设视角、光源编辑器、元素开关
踩的坑
useState vs useFrame
一开始在组件里用 useState 存动画状态,结果每帧都触发 React 重渲染,性能炸了。后来全部改成直接操作 Three.js 对象的属性,只在需要 React 响应的地方(比如 dev controls)才走 store。
生产环境 store 订阅
开发阶段一切正常,但发到生产后 profiling 发现大量不必要的 store 订阅。解决方案是导出 PRODUCTION_DEFAULTS 常量,生产环境直接引用,完全不触发 Zustand 订阅。
粒子系统性能
初期用了 transparent: true + opacity,粒子多的时候排序开销很大。换成 AdditiveBlending + depthWrite: false 后性能好很多,视觉效果反而更好——雨滴自然发光。
调试体验
这个必须说,leva 这个库太好用了。
通过 createRoot 挂载独立的 React 节点,完全不影响主应用。开发时可以实时调整:
- 相机位置 / FOV
- 视差强度 X/Y
- Bloom 强度 / 阈值
- 场景元素开关
- 每个光源的独立参数
- 8 个预设视角一键切换
调好之后直接导出配置到 console,复制粘贴成生产常量。
效果
最终效果就是现在你看到的首页——打开博客,第一眼是一个沉浸式的赛博朋克房间,鼠标移动有视差效果,滚动时 3D 场景自然淡出到文章列表。
移动端也做了适配,低端设备会自动降级。
下一步
这只是阶段一。后续计划:
- 阶段二:书架上的书对应博客合集,点击可跳转
- 阶段三:文章变成墙上的全息面板
- 阶段四:整个首页变成可自由探索的 3D 空间,多个房间对应不同合集
先不急,先把现在这个打磨好。
代码全在 GitHub 上,有兴趣的可以去看 src/components/cyberpunk/ 目录。有问题评论区聊。
站长