← 返回主页
AI Three.js 程序化生成 游戏

鹈鹕环岛记:给那只骑车的鹈鹕,造一座真的岛

2026.10 · by Easin

上个月,我让 9 个国产模型画「一只骑自行车的鹈鹕」,横评了谁画得对。这个月换个问法:能不能不画了——直接造一个世界,让那只鹈鹕真的骑上去?于是有了《鹈鹕环岛记》:纯 Three.js,约 2800 行,零模型、零贴图、零音频文件,一只戴水手帽的鹈鹕、一辆红色自行车、一座半径一公里、会呼吸的岛。

实机演示 60s:出发 → 车铃 → 电影机位 → 漂移 → 时间加速骑进夕阳

一、从一道考题到一座岛

上一篇的结论是:文生图组会「骑」但细节乱,SVG 组结构对但动作僵,最关键的一道采分点始终没被真正拿下——腿和踏板的关系。既然画不准,干脆换赛道:把鹈鹕、自行车、踏板全部变成三维空间里真实的几何体,腿的位置每一帧用 IK 解出来,想错都难。

设定也随之展开:岛要足够大(半径约一公里,骑完环岛公路要一两分钟),东西要足够多——环岛公路、十栋小屋的渔火村、夜间旋转光束的灯塔、雪线观景台的主峰、风力发电机、荷兰风车、观海栈桥与帆船、沙滩篝火、317 米跨海大桥与离岛「鹈鹕岩」、两只热气球、海鸥编队、蝴蝶与萤火虫,以及 40 条等它收集的小鱼。外加 15 分钟一整天的昼夜循环。

而整座岛最硬的一条约束是:零美术资源。没有模型、没有贴图、没有音频文件,所有几何用基元拼、颜色用顶点色写、水面天空用 shader 画、连风声浪声车铃声都是 WebAudio 现场合成的。Everything is code.

二、先玩再说

按键功能 W / A / S / D(或方向键)骑行与转向,Shift 漂移,Space 小跳 C视角:3/4 追尾 → 电影低机位 → 自由环绕(拖拽+滚轮) T时间流速 1× / 40× / 暂停——用 40× 看日落,是本作必玩项 F / M / P车铃 / 小地图 / 拍照模式(电影黑边+隐藏 HUD)

路面和草地抓地力不同,坡度会真实地拖慢或加速你;撞进草丛会扬起草屑,漂移会拖出尘尾。追尾相机带一点侧偏和速度 FOV,骑得越快视野越广。

三、这座岛是怎么「长」出来的

整个岛只有一个函数说了算:terrainHeight(x, z)。simplex 噪声先做域扭曲,让海岸线歪歪扭扭;再套一层海岛掩膜,指数取 1.28 让海岸坡度放缓、堆出宽沙滩;主峰和离岛各是一枚高斯凸起;村庄和灯塔海角做「台地」压平;最后,跨海大桥和栈桥的桥面直接写进高度场。地形网格、植被散布、建筑地基、骑行物理、小地图——万物都查这同一个函数,所以车轮永远贴地、贴桥,不存在「模型和地形对不齐」这种事。

海面是一支手写的 shader:两层反向滚动的程序化法线叠出波纹,菲涅尔把天空色抹在水面上,太阳和月亮各有一路 glitter 高光;岸边那圈会闪动的泡沫和浅滩的透亮,来自一张 256² 的烘焙高度图——shader 里拿「海平面减地形高度」算深度,深度小就有泡沫、就透明,看到底下的沙。

昼夜系统刻意做得极简:按太阳高度角插值的 5 个配色关键帧(夜→晨昏→金时→昼),一份调色板同时喂给雾色、水面三色、阳光颜色和半球光。于是黄昏来临时,天、海、雾、光一起变金,不需要任何额外的特判。Sky 物理天空配合 PMREM 环境光每 2.5 秒重生一次;星星和月亮各是一个 Points 和一个球;阴影相机不覆盖全岛,而是跟着玩家走,保证近处的影子永远清晰。

6000 多株植被全是 InstancedMesh:棕榈、阔叶树、高山松、灌木、岩石、野花、草,每个物种两三个变体,顶点色 + 一维 aSway 风摆权重,合并成一个几何体一次画完。风是往标准材质里 onBeforeCompile 注入的十几行 GLSL,相位来自实例位置,所以每棵树摆得都不一样。建筑同理:基元拼装 + 合并 + 顶点色,窗户单独实例化,入夜改一个 emissiveIntensity,全村的窗火一起亮。

渔火村白昼
渔火村白昼:串灯、市集、水井与远处的海
渔火村夜晚
同一座村,22:24:窗火、串灯与银河

四、鹈鹕:把「骑」这个动作做实

上一篇的采分点是「腿要够到踏板」。这次它不是画出来的,是解出来的:曲柄转到一个角度,踏板的位置解析可算;髋到踏板之间,膝关节用平面圆交一步求出;大腿小腿两根骨骼,各自朝两个关节点摆正——每帧一次双骨 IK。加速时踏频变快,倒车时腿反着蹬,跳起来时脚会离开踏板。

翅膀不是装饰:末端目标点始终锁在车把握把上,转向时车把转,翅膀跟着摆。头会看向转向方向,身体随踏频颠簸、随速度压弯。收集到的小鱼直接插进车筐里,最多五条——收集反馈长在角色身上,比任何 UI 都直观。

顺手还它一件上个月没人给的东西:水手帽。上个月最强的一张图给鹈鹕穿了小皮鞋,这次轮到帽子了。

跨海大桥
跨海大桥:护栏、桥拱、桥墩全部程序化,鹈鹕正骑向离岛鹈鹕岩

五、开发实录:四个值得记下来的坑

这种全程序化项目,写完只是开始,「跑起来看一眼」才是主战场。四个印象最深的:

🛑 离岛凭空消失。离岛的高斯凸起一开始加在海岛掩膜之前,而它在远海、掩膜值≈0,整个离岛被乘成了一片海底平地——表现是「跨海大桥的终点悬在海上」。修法:离岛项必须加在掩膜之后。教训:叠加在掩膜外的地物,永远写在掩膜乘法外面。
🛑 大桥搭错了岸。找跨海桥的岸点,直觉是从外海往回走、撞到第一块露出的陆地就停——结果先撞上的是离岛自己的山坡,桥变成了离岛内部一段 70 米的短匝道。修法:从主岛内侧向外走,记录最后一块露出水面的陆地。
🛑 整屏蒙雾。白墙、白鹈鹕在太阳直射下辐射度轻松超过 2,而 Bloom 阈值设在 0.85——线性 HDR 空间里几乎一切都超标,全画面泛光,像隔了一层纱。阈值提到 1.15 的瞬间,世界就清爽了。这是全程序化渲染最反直觉的一坑:调色参数在 HDR 下的「合理值」和直觉差得很远。
🛑 海边的同心圆波纹。内陆一些 h≈0 的洼地上,水面、泡沫与地形三者在深度上互相干涉,渲染出一片彩色莫尔波纹。修法两行:水面透明度与泡沫都乘一个「离岸渐隐」系数(smoothstep(1.6, 0.3, 地形高度)),内陆洼地直接不画水;顺手把水体 depthWrite 关掉,岸线反而更干净了。

还有一个工程外的花絮:项目里 player.js 被本地安全扫描器连续误判「命令注入」拦截写入,最后按既有经验走 Node REPL 落盘——AI 给 AI 写代码,连审查都是 AI,时代感这块是拉满了。

六、想复刻一座?提示词在仓库里

📐 仓库的 REBUILD_SPEC.md 把整座岛拆成 9 个可交付模块(噪声→地形→天空→植被→建筑→角色→特效→玩法→主循环),每个模块给公式、参数和验证标准;附录 A 是一份可以直接投喂给任何模型的复刻提示词,连上面四个坑都作为「已知地雷」写在了里面。拿去考考你的模型,欢迎在 issue 里晒成绩。

最后照例交代录制方法:页面里 canvas.captureStream(30) 接 MediaRecorder 录 WebM,ffmpeg 压成 2.8MB——比任何录屏软件都省事。


试玩入口:bayshier.github.io/pelican-isle · 仓库:bayshier/pelican-isle · 前传:鹈鹕骑自行车测试:9 个国产模型横评