本文是给家里小朋友做「太阳系漫游」网页(直达)过程中的技术调研与实践记录:先简单梳理浏览器 3D 技术的发展脉络,然后看两份有代表性的资料——three.js 官方的
llms.txt和一个用 WebGPU 手写引擎的钓鱼游戏 Tidewater,最后拆解我们自己的太阳系架构与踩坑实录。全文无框架、零构建依赖,代码都在 GitHub。
一、浏览器 3D 的十五年
把时间线拉长看,网页 3D 大致经历了四个阶段:
- 插件时代(2006–2011):3D 内容靠 Flash(Away3D)、Unity Web Player、Java Applet 这类插件承载,浏览器本身对 3D 一无所知。
- WebGL 时代(2011–2022):2011 年 3 月 Khronos 发布 WebGL 1.0(基于 OpenGL ES 2.0),浏览器第一次原生拥有 GPU 渲染能力;同年 mrdoob(Ricardo Cabello)开源了 three.js。2017 年 WebGL 2.0 与 glTF 2.0 格式先后落地,前者带来实例化渲染等能力,后者统一了 3D 模型的「JPEG」标准。
- WebGPU 时代(2023– ):2023 年 4 月 Chrome 113 默认启用 WebGPU——对标 Vulkan/Metal/DirectX 12 的现代图形 API,支持计算着色器(Compute Shader)和间接绘制,WGSL 取代 GLSL。three.js 随之推出 WebGPURenderer 和 TSL 着色语言。
- AI 与 3D 汇合(进行中):一方面官方文档开始为 LLM 优化(下文的 llms.txt),另一方面 Text-to-3D / 图生 3D 模型工具开始进入生产流程。
工具链层面同样在演进:从「全局脚本 + 多个 script 标签」到 ES Module + Import Map,从手动管理矩阵到场景图/材质系统,从 GLSL 手写着色器到节点式着色语言。这些变化决定了今天写一个网页 3D 项目的「正确姿势」。
二、three.js 的 llms.txt:给 AI 看的官方文档
2.1 llms.txt 是什么
llms.txt 是 2024 年由 Answer.AI 提出的社区约定:网站在根路径放一个 Markdown 索引,供大语言模型快速定位权威内容——因为直接把整站 HTML 塞给模型,噪音太大。
three.js 官方文档现在提供了 threejs.org/docs/llms.txt,开头第一句就是「生成 three.js 代码时请遵循以下指南」。它同时承担两个角色:
- 防坑指南:用 WRONG/CORRECT 正反代码对照,纠正 LLM 最常犯的过时写法;
- 文档导航:按板块列出官方手册与 API 的链接,让模型(和人)按需深入。
2.2 结构概要
整份文件的组织如下:
| 板块 | 内容 |
|---|---|
| Instructions for LLMs | 四条强制规范:Import Map 写入方式、WebGLRenderer vs WebGPURenderer 的选择标准、TSL 着色语言、NodeMaterial 材质族 |
| Getting Started | Installation / Creating a Scene / Fundamentals / Responsive Design |
| Renderer Guides | WebGPURenderer 指南 |
| Core Concepts | TSL 指南与参考、动画系统、模型加载、场景图、材质、纹理、灯光、相机、阴影 |
| Essential API | 按 Core / Scenes / Cameras / Renderers / Objects / Materials / Geometries / Lights / Loaders / Controls / Math 分类的 API 链接 |
其中最有时代感的两条规范:
① 引入方式——明确禁止 LLM 最爱生成的旧式写法:
1 | <!-- WRONG:LLM 十有八九会生成这个 --> |
② 渲染器选择——默认用 WebGLRenderer 保证兼容性;只有需要 TSL 自定义着色器、Compute Shader 或节点材质时才上 WebGPURenderer(且必须 await renderer.init())。TSL 的目标之一是让同一份着色逻辑在两个渲染器间迁移。
给 AI 看的文档,本质上是把「这个生态认为的正确姿势」固化成机器可读的约束——对我们人类的价值是:它是这个生态当前最佳实践的一份浓缩清单。
三、Tidewater:看看 WebGPU 能把网页做到什么程度
如果说 llms.txt 是「怎么正确写」,那 Tidewater(作者 Daniel Greenheck,MIT 协议)就是「能写到什么程度」的样本:一个直接跑在 WebGPU + WGSL 上、自研渲染引擎、零框架的岛屿钓鱼游戏——驾船出海、抛竿、搏鱼、天黑前把渔获卖给码头边的 Joe。
几个让我印象深刻的技术点:
- 水体:Tessendorf 谱方法的四级联 FFT 海洋(含泡沫、白帽、风条纹、涌浪),加上深度感知的破碎浪、浅水上冲流、船尾迹和海床焦散——基本是主机游戏的配置;
- 大气与光照:基于物理的 Hillaire 2020 大气模型(太阳/月亮/星空)、体积云及其投影、级联阴影 + 屏幕空间接触阴影、GTAO 环境光遮蔽;
- 后处理链:TAAU 时序超分 + 锐化、bloom、自动曝光、运动模糊、水下合成;
- 工程化:Vite 构建、
src/engine|ocean|sky|world|post|game分层清晰、无头引擎冒烟测试、大量?noClouds/?noSim类降级开关、动态分辨率。
页面有一条很诚实的提示:首次启动要编译数百个着色器,「可能需要一两分钟」,之后靠浏览器缓存加速。这其实是 WebGPU 应用当前普遍的代价——着色器编译与管线缓存策略,会直接影响用户的第一印象。
它和 llms.txt 一起构成了一个有意思的对照:three.js 在把「正确姿势」标准化以便普及,而 Tidewater 们在探索标准化之外的极限。两者都值得写业务代码时对照着看。
四、本项目:给小朋友的太阳系
有了上面的坐标系,再回头看这个项目要做什么:给 5~10 岁的小朋友做一个「太阳系漫游」——可以选星球抵近观看、切换视角看整个太阳系旋转、抵近看地月旋转、每个星球有中英双语说明。成品在这里(入口在博客导航)。

4.1 约束决定架构
三条约束几乎决定了所有技术选择:
- 观众在国内、以平板和手机为主 → Three.js 必须本地 vendored(不走 CDN),页面不能有国外依赖;
- 面向 5 岁小朋友 → 没有文字化 UI(零件/信息全是画出来的或图标化)、没有失败状态、暗面不能黑到看不见;
- 和游戏合集同一个部署体系(Hexo 产物仓库 + rsync)→ 页面放
source/games/solar-system/,用skip_render: games/**原样拷贝。
最终结构非常克制:
1 | source/games/solar-system/ |
4.2 几个核心设计
数据驱动的天体表。太阳、行星、月球都是同一份 BODIES 数据表的实例:半径、轨道、自转、轴倾角、贴图类型,以及中英双语的说明文案。信息卡、底部胶囊按钮、射线拾取全部由这张表驱动——加一个天体就是加一行数据。公转周期按开普勒第三定律压缩(T ∝ a^1.5),水星飞快、海王星缓慢,比例关系是真的:
1 | // 开普勒第三定律的玩具版:轨道半径决定角速度 |
程序化贴图 + 真实贴图的混合。木星条纹、火星锈斑与极冠、水星陨石坑全部用 canvas 逐行生成(512×256 一次性生成,零外部资源);只有地球和月球用 NASA 公有领域贴图。小朋友看到的土星不是「贴图球」,而是条纹 + 大红斑 + 冰环的组合。

自写轨道控制。拖拽旋转、滚轮缩放、双指捏合,加起来六十行——不引入 OrbitControls 附件,vendored 目录里只有一个 three 模块文件。聚焦某个天体时,相机会被摆到「太阳 → 天体」的延长线上(略抬高),保证看到的是被照亮的一面,地月同框时月球轨道环也一目了然。

面向 5 岁的视觉兜底。环境光调到 1.0,让行星暗面呈深蓝而不是纯黑;行星名用 sprite 标签(中文 + 英文)随场景旋转,可一键关闭;所有交互目标都是大按钮。
4.3 单文件打包:Import Map 的另一种用法
和游戏合集一样,这个页面也要有「双击就能玩、微信转发就能开」的版本。做法是把 three.js 整个模块文件变成 import map 里的 data URL:
1 | <script type="importmap"> |
main.js 里的 import * as THREE from 'three' 一个字不用改,贴图路径在打包时替换为 data URL。最终 dist/solar-system.html 约 1.9MB,无任何网络依赖。
4.4 踩坑实录(保存时间最长的三条)
坑一:白球事件——「赋值写在了错误的重构里」。上线后实测地球是纯白球体。排查链路:屏幕中心射线拾取确认「白球确实是 Earth 网格」→ 浏览器内采样贴图文件确认「文件和解码都正常(2048×1024,均值 RGB 84,92,105)」→ 材质探针确认 map 已绑定——最后发现是一次重构把 earthMesh.material = realEarth 这行赋值弄丢了:带贴图的材质创建出来了,但从未挂上网格,场景里一直渲染的是占位用的白球。教训:「材质已创建」和「材质已挂上网格」是两件事,肉眼断言不可靠,要用射线拾取+像素采样这种可复现的证据链。
坑二:three r155+ 的物理光照单位。PointLight 的 intensity 语义在 r155 之后变成物理单位,旧教程里的数值直接照抄会过曝——地球被冲成白球有一半功劳在它。最终方案:点光 1.15 + 环境光 1.0 + ACES 色调映射(exposure 0.9),过曝和死黑两头都压住。
坑三:无头测试里 rAF 是冻结的。自动化截图时 requestAnimationFrame 驱动的动画可能一帧都不跑。解法和官方测试套件思路一致:暴露一个手动推帧的钩子,测试里循环调它——这比在测试环境里折腾虚拟时间的 rAF 可靠得多。
五、结语
回到开头的时间线:从插件时代的 Flash 到今天一个 5 岁小朋友可以在 iPad 上拖动旋转的太阳系,浏览器 3D 用十五年走完了「能力补齐 → 生态标准化 → 极限探索」的三段路。three.js 的 llms.txt 代表前两段的沉淀——把正确姿势文档化;Tidewater 代表第三段——在标准之上把实时渲染推到主机游戏的配置。
对我们这种「给小朋友做一个页面」的项目来说,结论很简单:用 three.js + 本地 vendored + 数据驱动的架构,就在「正确姿势」的安全区里;而 Tidewater 们展示了这条路的天花板有多高。