AI 生成 3D 场景拆解 01:Tideglass Cove
Revio ·
学习 Techartist 的 Tideglass Cove,按地形、岩石、植物、房车、水体、动物、光照和镜头拆解程序化实现与关键公式,并将共享空间规则、建模方法和动效经验整理为可直接调用的 Skill。

Tideglass Cove 是一个可以交互的风格化海湾。棕榈、房车和石拱围绕沙滩展开,前方的水体剖面让我们能直接看到海草、鱼群和鳐鱼。拖动可以旋转场景,滚轮可以缩放;还能切换黄昏、调整潮位、点击水面制造涟漪、打开房车门,或让两只海豚跃出水面。
本篇参考学习的 Demo 来自 Techartist(GitHub 用户 iamtechartist),原地址为 github.com/iamtechartist/tideglass-cove。场景设计与原始实现归原作者,项目采用 MIT 许可证,本文配图来自原 Demo 的运行截图。
我们从这个案例开始学习如何用代码构造 3D 场景:先看技术栈,再逐项拆解地形、岩石、植物、房车、水体和动物,最后把可复用的方法整理成 Skill。原仓库没有说明作者使用了哪些 AI 工具或提示词,本文的 AI 复用方法来自我们的源码学习。
技术栈
项目入口是一个静态 index.html,通过 import map 映射本地 vendor 目录,直接运行原生 JavaScript ES Modules。源码自带的 Three.js 版本是 r185,场景行为由项目自己的代码控制。
| 层次 | 实际使用的技术 | 承担的工作 |
|---|---|---|
| 场景与几何 | Three.js BufferGeometry、Shape、ExtrudeGeometry、TubeGeometry、RoundedBoxGeometry | 构造地形、植物、车辆和动物 |
| 渲染后端 | WebGPURenderer | 优先 WebGPU,支持 WebGL 2 回退与强制选择 |
| 材质与动画 | TSL 节点、Standard/Physical/Basic NodeMaterial | 程序纹理、顶点位移、光学与颜色控制 |
| 相机交互 | PerspectiveCamera 与 OrbitControls | 三个预设镜头、环绕、缩放与阻尼 |
| 光照 | 方向光、半球光、点光源、程序环境图与 PMREM | 主光、补光、环境反射与局部灯光 |
| 动态数据 | Uniform、DataTexture、InstancedMesh | 时间、潮位、湿度历史、接触信息、水滴实例 |
| 行为与碰撞 | 项目自己的运动状态机与投影修正 | 海豚跳跃、鱼群分离、避开岩石和场景边界 |
TSL 是 Three.js Shading Language。这里用 JavaScript 的 sin()、mix()、smoothstep() 等节点写出着色器表达式,再交给 Three.js 转成对应后端的着色器。它让材质可以复用海岸、水面与焦散函数,也让同一份逻辑面向 WebGPU 和 WebGL。参见 Three.js 的 TSL 说明 与 WebGPURenderer 文档。
程序化生成
场景资产全部由程序生成。这个版本没有加载 glTF、OBJ、FBX 模型,也没有导入图片贴图或现成 HDR 环境图。海岸、岩石、棕榈、房车、海豚、鱼和水花,都是运行时通过几何构造函数生成的;表面颜色与细节来自节点材质、数学函数和运行时生成的数据纹理。
布局仍由作者明确设计:他先确定了海湾轮廓、房车位置、五棵棕榈的高低与倾斜、石拱的体块和配色,再用算法填充叶片、岩面、浪和鱼的细节。我们可以把它理解为将美术设计写成参数与规则:控制大形状,再由程序补充细节。
例如棕榈使用一张固定的 palmSpecs 表,房车固定在营地一侧,石拱固定在另一侧。随机部分则使用带种子的伪随机函数,默认种子为 8263,水花另用 9865。只要代码、种子和调用顺序一致,这些生成结果就可以重复;插入新的随机调用仍可能改变后续结果。
本文分析的版本为提交 17f7d97a8b62d8410e70951d51dae134da3e02a8,阅读日期为 2026 年 10 月 10 日。后文源码链接固定在这个提交,便于对照学习。
模块组织
场景启动时,依次构造地形、石拱、植物、房车、水下生物、水花和海豚;合并适合合批的静态物体后,再创建水体。动画循环更新共享参数、动物行为和相机,执行反射、无水场景与最终场景的渲染。
我们先从 core.js 的共享函数和 main.js 的组装流程看起,再进入各模块。
| 模块 | 阅读重点 |
|---|---|
| core.js | 海岸线、高度场、CPU 与 GPU 水面函数、湿度历史、程序材质与焦散 |
| terrain.js 与 sculpt.js | 剖面地形、多层岩石、隐式石拱、岩石接触纹理 |
| plants.js | 棕榈叶的参数曲面、层级风动、地被与海草 |
| camper.js | 曲线挤出车身、轮拱与窗洞、内部结构、门轴与遮阳篷 |
| water.js | 水面和侧面、反射、屏幕空间折射、吸收与泡沫 |
| life.js、dolphins.js、splash.js | 鱼群、鳐鱼、海豚状态机、入水检测与喷溅 |
| collision.js、optimize.js、main.js | 碰撞包络、合批、镜头、交互与质量控制 |
地形
地形是其他模块的基础。沙滩与海底共用一张连续曲面,植物落脚、水面接触和动物避障都要采样它的高度。
海岸线
作者先用 shore(x) 描述海岸位置,再用 d = z - shore(x) 判断一个点处于海岸的哪一侧。
海岸曲线是一个抛物线加两组正弦扰动:
shore(x) = -0.55 + 0.062x² + 0.17sin(0.85x) + 0.06sin(2.2x)
d = z - shore(x)
抛物线形成海湾的大弧度,两组频率不同的正弦波打破规整形状。这里的 d 是沿 z 方向的偏移,方便计算,却不是到海岸曲线的严格最短距离。
高度场
基础高度的主项是 -0.91 × tanh(0.53d),从陆地平滑过渡到水下;再减去 0.93 × smoothstep(0.8, 4.9, d),让远离岸边的海底进一步降低。两个高斯函数分别抬起一处沙丘、压低另一处水下凹地,最后叠加振幅约 0.011 个场景单位的正弦起伏,形成沙纹和轻微不平整。
把这些项放在一起,高度函数就有了清楚的分工:
地形高度 = 岸坡过渡 - 深水降低 + 沙丘 - 凹地 + 小幅沙纹
高斯隆起 = A × exp(-((x-x₀)²/a + (z-z₀)²/b))
高斯函数适合调整一个局部区域:中心附近影响大,向外自然衰减,不容易出现硬边。营地则有另一种处理:用两个平滑遮罩相乘得到矩形区域,再把其中的高度混合到 0.87,让房车附近的地面比较平整。石拱下方通过椭圆遮罩 grottoBasin() 接入浅盆地,避免拱脚和海底完全脱节。
这里反复使用的平滑函数是三次 Hermite 插值:
t = clamp((x-a)/(b-a), 0, 1)
smoothstep(a,b,x) = t²(3-2t)
mix(A,B,t) = A(1-t) + Bt
相比直接判断 x > a,它在两个端点处的斜率为零,适合做海岸过渡、材质渐变和镜头移动。项目的 CPU smooth() 与 GPU smoothstep() 使用同样的基本关系。
网格与剖面
地形网格从中心向边界逐圈展开,默认 68 层。外围轮廓来自 Three.js Shape 的二次曲线、贝塞尔曲线与两条刻意保留的前方切面;长边再按约 0.10 的间距细分。这是一张围绕固定轮廓生成的径向网格,适合这个小型展示场景;直接推广到任意凹多边形前,还需要重新考虑三角化方法。
场景像一块从海岸取出的标本:上面是营地和沙滩,下面可以看到水与岩层。为了得到这种完整体积,地形同时构造顶部、侧壁和底部。
侧壁从每个边界点的地表高度向 foundationY(x,z) 插值,共 12 层。地基高度随 z 改变,因此底部不是水平薄板,而是有方向性的抬升曲面。底面再用 24 层径向网格封闭,并在外圈复用侧壁落点。
细节也围绕这个剖面服务:侧壁有少量岩石碎片,主体埋在断面内部,只露出断裂尖端;前方切面保持干净,方便观察海底与生物。岩层材质沿高度叠加正弦色带与细窄接缝,读起来像有沉积层次的石灰岩。
水的侧面沿同一圈地形边界生成,上沿顶点跟随水面,下方接到局部海底;着色器继续剔除低于地形或高于水面的片元。上表面与侧面共享水面函数,因而浪动时不会一个在动、另一个停在原地。
从全景看,前方切面同时露出岩层、水和鱼,让海湾内部也成为画面的一部分。
岩石
这里有两类需求:碎石需要简单而有变化的轮廓,石拱需要连通的岩体和空腔。它们采用了不同的建模方法。
碎石
小岩石由九边形截面搭起来。每个角的半径加入两组正弦扰动,再沿四个高度层缩放和偏移:底部较收、腰部鼓起、顶部进一步收窄,顶面也带倾斜。索引网格转成非索引后重新计算法线,让不同三角面保留棱面感。
岩石位置有两类:靠岸的大石块使用明确坐标,作为构图与接触对象;水下石架周围的碎石使用有种子的局部随机分布。贝壳则沿潮线以上的窄带生成小型扇形网格,避免把整个沙滩均匀撒满。
石拱
石拱需要左右支柱、顶部岩盖和内部空腔连成一个整体,作者在 archField(x,y,z) 中定义一个类似有符号距离场的隐式标量场,再提取零等值面。通常把负值看作岩体内部、正值看作外部;这里包含位移和多种组合,不保证数值处处都是严格的欧氏距离。
这种写法的基本运算很容易理解:
并集 min(A,B)
交集 max(A,B)
从 A 挖去 B max(A,-B)
项目用多个平面不等式的 max() 定义左右支柱、顶板和后壁,再通过平滑最小值 smin() 把它们连接起来:
h = clamp(0.5 + 0.5(B-A)/k, 0, 1)
smin(A,B,k) = mix(B,A,h) - k h(1-h)
k 控制接合处的圆滑范围。接着定义一个带曲线拱顶的空腔,使用 max(rock, -cavity) 从岩体里减去它。斜向平面切掉过整齐的角,水线附近的高斯项削弱岩脚,几条窄高斯带形成层理,椭球场切出局部缺口,最后加入两档幅度很小的正弦侵蚀。
这里值得记住的是生成顺序:先定义肩部、顶板、空腔和断裂方向,再加入小幅侵蚀。噪声的作用是丰富岩面,石拱的形状仍由前面的结构决定。
网格提取
网格提取算法是 Marching Tetrahedra。源码把每个采样立方体分成六个四面体,检查四面体各条边的两端是否异号;若异号,就按线性插值找到零点:
t = f(A)/(f(A)-f(B))
P = A + t(B-A)
normal ≈ normalize(∇f)
三个交点组成一个三角形,四个交点则排序后拆成两个。法线通过中心差分近似场的梯度。当前石拱传入的采样步长是 0.066;它决定网格细节与构造成本。渲染时仍然是普通三角网格,项目没有用逐像素 SDF 光线步进渲染石拱。
植物
棕榈树干
五棵棕榈的参数分别规定位置、高度、倾斜、叶长和叶柄数量。树干中心线沿高度参数 t 弯曲,水平偏移主要用 t²,所以根部稳定,越接近树冠偏移越明显。
树干沿中心线生成 100 段、每圈 14 份的截面,半径逐渐收细;根部加粗,高频正弦塑造环状生长痕,截面的角向扰动打破圆柱感。树根用短曲线管连接到真实地形高度。
叶片
棕榈叶由叶轴和两侧的小叶组成。先为羽状叶建立弯曲的主轴,中心线包含水平伸展、向上抬升、末端下垂和少量侧向卷曲。再沿主轴两边长出一对对小叶。小叶的长度遵循 sin(πt) 的幂函数,中段宽,根部与尖端窄;横截面中间略高、两侧略低,形成折叠而不是完全平的纸片。
作者还加入了几种叶片变化:少量小叶缺失,左右位置略错开,叶长与扭转不同;最后三片大叶被设为较短、较直立的年轻叶,旧叶的部分末端混入少量黄褐色。树冠的叶基、椰果、叶柄也有对应几何,避免叶片像悬在树干旁边。
风动
叶片生成以后,还要让它们随风摆动,并保持叶根与叶轴相连。源码把运动分为三个层次:
| 运动层 | 控制量 | 作用 |
|---|---|---|
| 树干整体偏移 | 主体权重 t² | 根部不动,树冠随低频风摆 |
| 叶柄弯曲 | 沿叶轴的权重 t² | 叶柄根部跟随树冠,自由端摆动更大 |
| 小叶颤动 | 小叶长度权重 q² | 小叶根部保持连接,叶尖有细微抖动 |
每个顶点还携带 windAnchor 四维属性和 windWeight 三维属性,记录它所属棵树的锚点、相位、叶方向,以及上述三种运动的权重。所有相连部分共享整体位移,只有自由端叠加更局部的变化。
我们复用这套风动时,需要保留这些锚点和权重,否则各部分独立摆动容易拉开接缝。动画通过 material.positionNode 在 GPU 上执行,没有骨骼、蒙皮和外部风动画贴图。
地被与海草
地被采用相似思路:贴地匍匐茎按地形高度取点,小叶通过叶柄接到茎上;海草则用细长条带,每个顶点存储 flex = t²,底部固定、顶端摆动。棕榈与遮阳篷受共享的 Breeze 参数控制,海草使用独立的低幅度周期运动。
房车与营地

车身
房车侧面先用 Shape 描出圆润的车顶、车头和底边,轮拱直接进入轮廓,窗与门用 holes 开孔,再由 ExtrudeGeometry 挤出薄壳,并给边缘加小倒角。窗框使用带内孔的另一张形状,玻璃单独放在孔内。
车顶沿侧面共有的贝塞尔边界展开一张曲面,再在横向加入轻微的正弦拱起。斜前窗同样先生成二维形状,再把顶点映射到 frontPoint(z,v) 定义的倾斜曲面上,让前窗框、玻璃与车身共享形状关系。
其余零件各取适合的方法:圆环生成轮胎和方向盘,圆柱生成轴、支杆和灯;RoundedBoxGeometry 生成内饰、座椅、柜体、后视镜与台阶。冲浪板沿长度生成逐渐收尖的截面,再抬起两端;折叠椅的四个落脚点分别采样地形高度。
材质
车漆颜色由材质函数生成。材质用世界高度减去车身基准高度,再通过一条很窄的 smoothstep 把下部陶土红和上部奶油色连接起来;微弱的高频噪声补一点油漆表面变化。窗玻璃使用 Physical NodeMaterial,transmission 为 0,透明由 0.43 的 opacity 实现。它让我们能看到内部,光学效果比后面单独实现的水体简单。
开门
门的局部坐标原点放在竖向铰链上,旋转门组即可开关,目标角度约为 0.56π。门角使用指数平滑靠近目标,房车内部保留座椅、柜子、桌板、方向盘和灯光,让开门后有内容可看。
遮阳篷
遮阳篷的下垂与细小褶皱写入初始网格,顶点柔性权重取 sin(πu)sin(πv):边缘权重为零,中间布面才能随风上下起伏。条纹通过局部 x 坐标上的正弦函数形成,高频噪声负责细微织物感。
水体
水体牵涉几何、光学和接触效果,也是这个场景中依赖关系最多的模块。我们按水面网格、波浪、反射、折射和岸边效果的顺序来看。
水面网格
水面几何先使用更细的地形网格,逐三角形裁切到地形高度不超过 0.48 的区域。遇到边缘交点时用 18 次二分逼近,避免大量水面三角形覆盖营地。这里的 0.48 是预留的最大湿区范围,实际潮位控制范围是 -0.18 到 0.22;运行时着色器再根据当前水面和地形高度剔除不该出现的水。
波浪
波浪通过三组方向正弦波叠加,改变顶点的 y 坐标。这种解析波形适合小型平缓水域,计算量也比较可控。三组波共享一张 WAVE_COMPONENTS 参数表:
| 分量 | kx | kz | 时间系数 ω | 振幅 A |
|---|---|---|---|---|
| 长波 | 0.24 | 1.65 | 1.02 | 0.041 |
| 次级波 | -0.46 | 3.05 | 1.37 | 0.020 |
| 风致细波 | 7.1 | 5.3 | -2.25 | 0.0048 |
w(x,z,t) = Σ Aᵢ sin(kxᵢ x + kzᵢ z + ωᵢ t)
水面高度 = 潮位 + shelter(x,z) × w + 拱下局部波 + 接触涟漪
这些波数、速度和振幅是视觉参数,没有强制遵循深水波的色散关系。shelter() 根据离岸位置、石拱附近的背风区和盆地遮罩减少波幅;风致细波再乘 breeze + 0.25。因此近岸与拱下更安静,外侧水面更活跃。
CPU 的 wave() 与 GPU 的 waveNode() 使用同一组系数:GPU 位移水面,CPU 检查沙地接触、海豚入水和水滴落回水面的时刻。我们让画面与接触检测共用这些参数,就能让海豚和水滴落在实际显示的水面上。
水面法线由波高梯度构造:
n = normalize(-∂w/∂x, 1, -∂w/∂z)
∂wᵢ/∂x = Aᵢ kxᵢ cos(kxᵢx + kzᵢz + ωᵢt)
原实现还叠加了两组更细的法线扰动,让光泽有小波纹。waveGradientNode() 对主要波与涟漪求导,但没有完整包含 shelter 的空间导数和拱下局部波的导数,因此是一种偏向视觉效果的近似法线。
反射
反射单独渲染到离屏纹理。相机关于平均潮位平面镜像:cameraY' = 2 × tide - cameraY,视线与 up 向量的 y 分量也翻转。水体暂时隐藏,避免把自身反复画进反射纹理。
随后修改反射相机的投影矩阵,做斜近平面裁切,把水面下方不该出现在镜面反射里的内容切掉。这通常称为 oblique near-plane clipping。源码还区分 WebGPU 与 WebGL 的投影深度约定,调整对应系数。
主水面用反射矩阵把世界位置投影到反射纹理,再由波动法线偏移采样坐标。这样棕榈和房车的反射有真实场景内容,也会随着小波扭动。镜像发生在平均潮位平面,细波只扭曲采样,所以它并不是逐波面几何精确求解的反射。
折射
场景另渲染一遍无水画面,保存颜色和深度。水面片元先根据法线得到屏幕 UV 偏移,再从深度反推采样点的视空间位置,并转回世界空间。
偏移不是无条件使用。代码检查候选采样点是否有有效深度、是否在潮位以下、是否位于当前水界面之后,以及偏移前后重建位置的差异是否过大。不满足时回退到原 UV,降低岸上房车和棕榈轮廓被水错误扭曲的情况。
这属于屏幕空间折射近似:利用当前无水画面制造折射偏移,屏幕外与被遮挡的内容无法从这张纹理恢复。它没有按 Snell 定律追踪完整的折射光路。
水色与高光
代码把穿水路径近似限制在三个距离的最小值:到无水场景深度命中点的距离、光线向上离开水面的距离,以及穿过海湾切面的距离。边界采用抽样轮廓的平面估计,最终路径限制在 0 到 14,适用于这个有限剖面。
随后使用 Beer–Lambert 型指数吸收:
T = exp(-σL)
σ = (0.78, 0.13, 0.079)
Cwater = Cbehind × T + Ctint × (1-T) × 0.55 × 深度修饰
红通道吸收系数最大,因此水后面的颜色随距离增加更容易失去红色,转向青蓝。这里的系数与颜色按场景风格选择,不能直接当成真实海水测量值。
表面反射与透射通过一个 Schlick 风格的视角权重混合:
F = 0.023 + 0.85(1-max(n·v,0.02))⁵
C = mix(Cwater, Creflection, F) + 太阳高光
俯视时更容易看见海底,贴近水面看时反射增强。这个式子借用了 Fresnel 的形状,但 0.85 是美术调节项,并非严格的 1-F₀。太阳高光则对镜面方向与视线的点积使用 650 次幂和 90 次幂,组合出很窄的亮点与较宽的弱光泽。
泡沫
泡沫由三项相加:岸边泡沫、岩石周围泡沫,以及浅水里的细小浪脊。它们都需要浪的到达相位和噪声遮罩,因而不会变成一条静止的纯白描边。
岸边部分先计算 depth = 水面高度 - 地形高度。深度很小时才能产生泡沫,再用主要长波的相位筛出浪涌上岸的时段,用动态噪声打碎连续色带。浪脊则用正弦的高次幂形成窄条,限制在浅水区。
岩石不能只用海底高度判断,因为石块与石拱是独立几何。作者在 CPU 上生成一张 160 × 128 接触纹理:根据当前潮位求出小岩石的截面轮廓,并采样石拱隐式场,把到接触边缘的近似距离编码到颜色通道中。
水材质将该通道从 0 到 1 解码为 -0.6 到 0.6,寻找接触轮廓附近的窄带,再叠加波相位与噪声,形成绕着岩脚出现的泡沫。潮位变化达到 0.007 才重新计算接触纹理,减少 CPU 工作;因此这个图跟踪平均潮位截面,细波带来的变化主要在着色器中修饰。
泡沫的总权重限制到 0.63,并按昼夜切换白绿或灰蓝颜色。这个参数选择让泡沫成为水体上的局部提示,不至于盖住海底和反射。
湿沙
浪退以后,沙地还会湿一段时间。为此,湿沙除了采样当前水面,还保存了接触历史。这里用一张 112 × 112 的 DataTexture 保存每个位置的湿度,约每 0.10 秒更新一次;明显的潮位变化也会触发更新。
每个纹素先把 height(x,z) 与 wave(x,z,t) 比较,算出当前浸湿程度,再与衰减后的上一帧湿度取较大值:
wetNew = max(currentContact, wetOld × exp(-Δt/11))
11 秒是指数衰减的时间常数,不表示到第 11 秒突然完全变干。反复被浪触碰的地方会继续保持湿润,刚退水的位置则逐渐恢复干沙。材质把暖浅色干沙混到较深的湿沙,粗糙度从约 0.98 降到 0.30;完全淹水后另用约 0.87 的粗糙度,并加入焦散颜色修饰。
焦散

焦散指水下常见的流动亮纹。项目用程序噪声近似这些图案:mx_noise_float 在这份 Three.js vendor 源码中映射到 Perlin 噪声。
做法是沿太阳方向把世界位置投影到一个二维坐标,再加低频时间偏移与噪声扭曲。两组不同尺度、不同时间方向的噪声相加,寻找它们接近零的窄带,形成细亮纹;另一项高斯乘积制造小型亮斑。低频 focus 遮罩控制哪里更集中,避免全海底密密麻麻地亮起来。
亮度还受入水深度、表面朝向、石拱遮蔽和昼夜控制:深处指数衰减,朝向太阳的面更明显,洞下和黄昏更弱。深度越大,图案尺度也改变,让效果有空间层次。
石拱内部又作了专门处理。材质用局部坐标遮罩选中洞内面,稍微偏向青蓝,并把类似焦散的动态图案加到一项很弱的 emissive 上;同时辅以一个小点光源。这是为洞内可读性增加的反射光暗示,没有实时求解完整的间接光照。
涟漪
交互涟漪最多保留八个事件,每个事件只存中心 x、z,发生时间和强度。事件循环使用槽位,点击很多次时新事件覆盖旧事件,不会无限增加计算量。
令 r 为片元到事件中心的距离,a 为事件年龄,涟漪高度可写成:
envelope = exp(-4.8(r-1.25a)² - 0.72a)
× smoothstep(0,0.16,a)/(1+0.45r)
ripple = 0.023 × strength × sin(11r-13.75a) × envelope
r - 1.25a 决定波环向外传播,指数项把能量集中在这个环附近,时间衰减与距离分母让波逐渐消失;起始 smoothstep 避免刚点击时突然跳出很大位移。它是解析波形,代码不保存每个网格点的速度,也不求解二维波动方程。
点击位置也做了处理。屏幕坐标先变成相机射线,再与平均潮位平面相交,连续四次根据采样波高调整平面,近似找到起伏水面上的交点。如果前面命中了陆地、石头或房车,就不透过物体制造水波;房车对象的点击会转去开门。
在黄昏,涟漪和泡沫还驱动一项弱青色发光。水体最终颜色直接加上这项颜色,形成类似生物荧光的提示,没有使用 Bloom 后处理;鱼后方的短促亮点则是另一个独立的小网格。
鱼群与鳐鱼
水下动物的外形由参数曲面与变化截面生成,游动再由顶点动画、路径和避障共同控制。
鳐鱼
鳐鱼先沿前后方向定义体宽,再横向采样展翼曲面,分别构造上表面与下表面,让中间躯干厚、翼边薄。尾巴沿 Catmull–Rom 曲线生成逐渐收细的截面,眼睛由小球补出。
翅膀运动写在顶点着色器里:正弦相位沿横向和纵向变化,振幅乘 abs(x)^1.8。越靠中心摆动越少,翼端上下起伏更明显。鳐鱼整体在一条缓慢的周期路径上游动,朝向跟随路径方向,再加很小的身体侧倾。
鱼群
鱼群共有十条鱼。鱼身采用沿长度方向变化的椭圆截面,尾鳍用二维形状挤出,背鳍和眼睛补出轮廓。尾鳍保留独立变换,其他固定零件按材质合并,因此鱼身移动、转向和摆尾可以分别处理。
鱼群行为包含随时间变化的目标点、预测分离、转向限速、速度平滑和碰撞修正。先看约 0.36 秒后的预测位置,距离过近就增加分离速度;最大速度限制为 0.43,最大转向速率为每秒 2.3 弧度。
随后做五轮相互穿插修正:两条鱼距离小于安全距离时,沿中心连线各移动约一半重叠量;如果速度仍在互相靠近,也沿该方向修正速度。它和 Boids 的“分离”思想接近,但没有完整实现经典 Boids 的邻居平均速度对齐、邻居中心聚合三规则。
避障
环境碰撞以简单几何包络为主:海底与水面构成竖向限制,岩石用扩大后的椭球,海豚用沿身体方向的胶囊,场景边界则取最近边上的安全距离。若点在椭球内部,代码把它投影回膨胀后的表面:
q = sqrt((dx/rx)² + (dy/ry)² + (dz/rz)²)
q < 1 时 P' = center + (P-center)/q
这是行为避让用的保守包络,包含可见岩石以外的余量,并非逐三角网格碰撞或通用刚体系统。对于固定海湾和少量动物,它足以减少穿石、穿底与互相重叠。
海豚
建模
作者沿海豚身体轴列出十个采样站,每站给定横向半径、纵向半径和截面中心高度,再使用单调三次 Hermite 插值连接它们。
在斜率同号时,节点切线使用相邻斜率的加权调和平均;斜率反号时设为零。这个形状保持方法可以减少普通曲线插值过冲,避免细嘴、颈部或尾柄附近出现异常鼓包。主体用 84 个纵向环、每环 32 份的椭圆截面生成,背鳍、胸鳍与尾鳍再由形状挤出或参数曲面补齐。
材质按局部高度混合浅腹部和深背部,鳍区再变深,粗糙度也不同。尾部运动依然由顶点位移实现:越靠近尾端,纵向权重越大;跳跃的不同阶段还改变摆尾强度。
跳跃
一次跳跃包含起跳前的准备、空中飞行和入水后的恢复。每只海豚都有下面的状态顺序:
巡游 cruise → 集合 gather → 接近 approach → 加速 power
→ 飞行 flight → 入水 entry → 恢复 recovery → 巡游
水下阶段通过三次贝塞尔曲线移动。项目给定起点、终点和两端速度,再转换成控制点:
B₀ = P₀
B₁ = P₀ + V₀T/3
B₂ = P₁ - V₁T/3
B₃ = P₁
这使前后阶段可以保持速度方向的连续性,减少路径连接处突然拐弯。双海豚手动跳跃使用不同的水道和相反方向,并在完成后换到另一侧的巡游区域。
飞行阶段则用重力抛物线:
x(t) = x₀ + vx t
y(t) = y₀ + vy t - 0.5 × 9.81 × t²
水下路径与空中弹道分别计算,再通过状态机衔接。身体朝向由速度向量建立局部坐标基,因此飞行上升时头朝上、下降时头朝下。
入水
入水检测取的是沿速度方向伸出的前端接触点,而非海豚中心。下降时,如果这个点低于 CPU 水面函数,就在最近一个时间步内做 16 次二分,找到更准确的接触时刻,再发出水花和涟漪。
海豚更新采用不大于 1/90 秒的子步,减少快动作漏过水面的问题。默认约第 12 秒开始一次自动跳跃,随后间隔约 28 秒轮换;手动双跳会延后下一次自动动作。Jump 按钮在动作进行时禁用,避免重入状态机。
水花
海豚碰水后触发两层效果。第一层是最多 480 个小球实例组成的水滴池,每个完整强度喷溅发出约 180 滴;第二层是向外扩展、向上抬起再塌下的冠状水膜,保留四个独立槽位,让两只海豚的事件可以同时存在。
水滴
每滴水记录初始位置、速度、延迟、大小和阻力系数。初始方向不是完全均匀的球形爆炸,还叠加海豚水平运动方向,形成沿入水方向偏移的喷溅。
水滴使用线性阻力与重力方程的解析解。设阻力系数 k、时间 a、重力 g,令 travel = (1-exp(-ka))/k,则:
x = x₀ + vx₀ × travel
z = z₀ + vz₀ × travel
y = y₀ + (vy₀ + g/k) × travel - ga/k
vy = (vy₀ + g/k)exp(-ka) - g/k
代码再让水滴朝向当前速度,并随速度拉长;落回共享水面函数以下或存活超过 2.2 秒就回收。InstancedMesh 的意义是共享一份球几何和材质,只更新各水滴的矩阵,不为每滴水建立独立绘制对象。
水膜
冠状水膜是一张 112 段、7 层的径向网格,每帧由 CPU 改写顶点。半径随时间扩大,高度用短抛物线起落,角向的 11、23、13 倍正弦制造不整齐的水缘;沿入水方向拉伸,另一方向压扁,透明度在约 0.57 秒内衰减。
这两层效果分别表现喷出的细水滴和短暂撑开的水膜,都由入水事件触发。它们使用明确的弹道、形变与回收规则,适合这种短促的展示效果。
风格与光照
构图
这片海湾的风格化主要体现在体积、比例和色彩的取舍。棕榈构成上方轮廓,奶油色与陶土红的房车提供视觉焦点,右侧石拱平衡营地,青绿水体铺在前景,有限厚度的岩层把所有内容收成一个小型景观标本。
我们从画面看到的这些关系,在源码中对应固定布局、五棵高低不同的棕榈、前方切面和受控色板。植物与海豚都有细密几何,风格化主要来自整体造型与配色。
材质细节
表面细节按尺度分工。岩石用低频噪声调色,中频正弦做层理,高频噪声产生孔隙和凹凸;沙地用更细的噪声与小幅沙纹。大量变化的幅度只在几个百分点以内,使大色块和轮廓仍然清楚。bumpMap() 主要改变光照法线,几何轮廓仍由真正的网格承担。
棕榈叶按小叶与横截面分配顶点色,再在背光时加入很弱的绿色发光,暗示叶片透光。这是一种简单的透光近似。石拱内部、水下焦散、灯笼与鱼尾亮点也都使用局部控制,避免整片场景一起泛亮。
环境光
光照包含强度 3.05 的暖方向光、半球补光、较弱的冷色方向光,以及约 0.25 的环境强度。环境反射图是运行时生成的 256 × 128 浮点纹理:天空、地平线与地面颜色渐变,加一个高斯太阳亮斑,再由 PMREM 预过滤,以便不同粗糙度的材质读取不同宽度的反射。
背景另用 screenUV.y 生成稳定的画面渐变。最终使用 ACES Filmic 色调映射,日间曝光约 1.03。项目没有配置景深、SSAO、Bloom、轮廓描边或体积雾后处理。
黄昏

Dusk 在日间与黄昏两套预设之间过渡。主方向光从暖色逐渐变冷,强度降到约 0.26;半球光转为蓝紫,侧向冷光稍微增强,环境纹理混到黄昏版本,曝光微调到 1.10。
房车内部灯和灯笼随黄昏参数增强,灯泡几何的 emissive 也提高;石拱内部点光源稍强,并叠加低频呼吸般的变化。水体颜色、泡沫和亮点同时转入另一套色彩,使场景各部分仍然协调。
昼夜、风和门角的过渡使用同一类指数平滑:
valueNew = valueOld + (target-valueOld) × (1-exp(-kΔt))
这样参数变化按秒控制速度,帧率波动时也能保持相近的响应。相较于每帧固定插值百分比,它更适合不同设备上的连续交互。
接触阴影
房车下面有独立的短距离接触阴影,按底盘与四个轮胎分别布置透明网格,把各顶点投到沙地高度。透明度按 UV 中心到边缘衰减,因此无论主光怎样变化,车辆下面都有柔和的暗部。鳐鱼也有一个跟随位置、取海底高度的阴影面,不过它按中心采样高度,没有完整贴合全部地形起伏。
沙滩上还生成了少量脚印和轮胎印,使用变形的小圆片与窄矩形,放到局部地形上方,配合低透明度材质。这些网格关闭投射和接收阴影,并标记为不可拾取,避免点击痕迹时干扰水面与房车交互。
阴影图也有明确取舍:太阳阴影使用 2048 × 2048,动画期间约每三帧刷新一次;房车内部点光源使用 256 × 256,并通过 layer 2 限定相关阴影对象,开门时才按节奏更新。它们提供实时阴影,同时控制额外渲染量。
镜头
默认全景采用 PerspectiveCamera,窄视角让它有类似微缩景观的感觉。宽屏垂直视场角为 33°,相机较远、位置偏高,减少近大远小的夸张感;近裁面 0.1,远裁面 100。
| 预设 | 相机位置 x y z | 观察中心 x y z | 画面重点 |
|---|---|---|---|
| Cove | 11.88 10.85 19.95 | 0 0.89 0.05 | 棕榈 营地 石拱 水体与岩层的整体关系 |
| Campsite | 5.0 5.3 10.8 | -2.08 1.53 -1.95 | 房车与营地细节 |
| Underwater | 1.1 0.1 12.9 | 0.35 -0.15 0.65 | 低机位查看水体剖面与海底生物 |
Underwater 是 UI 的名称。在默认潮位为 0 时,相机 y 为 0.1,实际上略高于平均水面,因此这个预设主要通过前方剖面展示水下,并非把相机放进水里后进行完整的潜水渲染。
当宽高比小于 0.65,视场角增到 47°,全景机位换到 (24.6,16.1,12.7),观察中心也调整;另外两种视图沿视线退远。接近方形的屏幕则把全景按宽高比继续拉远。这些处理比单纯改变 canvas 大小更进一步:作者实际为窄屏重新组织了画面。
镜头切换在 650 毫秒内完成,同时插值机位和观察中心,进度使用 s²(3-2s) 缓入缓出。OrbitControls 开启阻尼、关闭平移,限制观察距离为 8.5 到 39,极角限制在约 0.33 到 0.515π,让观众能看剖面,却不轻易翻到小岛底下。
自动环绕速度较慢;手动开始操作就取消自动环绕和镜头过渡。用户偏好减少动态效果时,场景默认暂停,镜头过渡缩短到 120 毫秒。
性能优化
这个场景有大量小叶和密集的石拱网格,水体又需要额外的离屏渲染。性能优化主要围绕合批、纹理尺寸、刷新频率和像素比展开。
合批
作者使用了三种合并方式。植物在生成阶段按树干、叶片和叶柄分别合并;普通静态对象按材质、阴影标志、拾取角色与图层分桶,烘焙变换后合并;动物则在自身坐标中合并固定零件,保留会动的尾巴等部件;房车门轴也保持独立。透明材质、依赖顶点位移的材质和动态父节点不能随意加入普通静态合批。
合批还需要检查材质是否依赖局部坐标。石拱内部材质设置 keepLocal,因为它的洞内遮罩根据石拱自己的 x、y、z 判定;如果直接烘焙为世界坐标后合并,这个遮罩就会失去原来的含义。
渲染质量
水体的无水画面按当前 drawing buffer 全尺寸生成,反射通常为其一半宽高,Light 档降低到 0.30 倍。常规动画中反射每两帧刷新,无水画面每帧刷新,最终主画面也每帧绘制。半宽半高对应约四分之一像素数,但实际工作量还受绘制对象、材质和阴影影响。
画面像素比也有上限:Auto 通常不超过 1.5,小屏不超过 1.35,High 不超过 1.8,Light 不超过 1。Auto 模式在持续偏慢后降低像素比,并将反射比例调到 0.32。这是一种基于最近帧时长的自适应降质,没有针对每个模型逐一切换 LOD。
调试
我们可以分层观察水体:URL 参数 layer=reflection、refraction、foam、sun 只输出对应水面分量,方便判断是反射投影、折射、泡沫还是高光出了问题。renderer=webgl 强制 WebGL 2;pause、at、night、view 和 tide 可用于固定场景状态。
当前结构仍有可以继续研究的地方:屏幕空间光学缺少屏外信息,水面法线采用局部近似,顶点风动与动物变形没有完整重建所有变形后的法线,鱼群使用成对检查,初始化还需要构建石拱网格并编译材质。后续扩大场景规模时,可以分别研究光学稳定性、法线修正、空间分桶和资产缓存。
学习总结
这个项目最值得我们带走的,是各模块如何共用一套规则。海岸同时影响地形、波幅和泡沫;水面同时参与画面、湿沙和动物入水;植物的连接关系又决定风动时哪些顶点必须一起移动。后续自己做场景时,可以先把这些关系建立起来,再增加资产与细节。
学到的经验
这里的房车、石拱和五棵棕榈都有明确的位置与比例。随机数主要补充叶片、岩面和水花的变化。我们复用时也可以先确定主体和镜头,再用带种子的局部变化丰富表面。
高度场适合连续陆海地形,截面适合石块和动物身体,带孔挤出适合房车,隐式场适合石拱。为每个部分选择合适的表示,比给所有对象套同一种生成算法更容易控制结果。
树干与叶片共享风动锚点,车顶与侧面共享曲线边界,水面与侧面共享波函数。这些接口决定细节增加以后,场景是否仍然连贯。
湿沙要记住刚才是否被浪触碰;海豚入水要给出准确的位置、方向和时刻,再触发涟漪与水花。只读取当前一帧的位置,往往不够表达这些变化。
全景看布局,营地近景看部件连接,低机位看水体剖面。反射、折射和泡沫还能分别输出,帮助我们找到具体哪一层需要调整。
可复用的 Skill
我们把这些方法整理为 procedural-3d-scene:主文件负责场景生成流程,案例文件保存 Tideglass Cove 的算法、公式、适用条件和源码位置。以后分析新的 Demo,可以继续添加案例,而不用把某个项目的尺寸和配色固定成通用规则。
下载完整 Skill · 查看 Skill 正文 · 查看 Tideglass Cove 案例
这个项目归纳进 Skill 的内容如下:
| Skill 部分 | 从 Tideglass Cove 学到的方法 | 后续可以用在哪里 |
|---|---|---|
| 构图与参数 | 固定主体布局、窄屏机位、有种子的细节 | 小岛、庭院、营地等展示场景 |
| 共享空间接口 | 连续地形,CPU/GPU 共用水面参数 | 落脚、避障、接触与材质一致性 |
| 资产生成 | 截面、曲线挤出、隐式场、叶轴与小叶 | 按形状选择程序化建模方法 |
| 材质与光照 | 分尺度纹理、程序环境图、昼夜联动 | 统一风格,保持整体配色 |
| 水体与接触 | 分层光学、岩脚距离图、湿度历史、事件涟漪 | 有岸线和独立岩石的小水域 |
| 动画与行为 | 分层风动、阶段运动、端点速度、入水事件 | 植物、游动动物、跳跃与喷溅 |
| 合批与画面检查 | 保留局部坐标、动态部件,调节离屏渲染 | 控制大量小部件和水体的成本 |
如何交给 AI 使用
在 Codex 中,将解压后的 procedural-3d-scene 文件夹放进 ~/.codex/skills/,就可以在任务中使用 $procedural-3d-scene。这里的 Skill 已经分开保存通用流程与案例细节,我们调用时只需要补充场景目标、技术栈和本次要完成的范围。
使用 $procedural-3d-scene,制作一个可以环绕查看的风格化海湾。
参考 Skill 中的 Tideglass Cove 案例,使用 JavaScript、Three.js 和 TSL。
场景包含沙滩、植物、营地和浅水,不导入外部模型或图片贴图。
先用占位体完成全景与竖屏构图,再实现连续陆海高度场。
定义共享的 terrainHeight、waterHeight、time 和 tide 接口。
本次先完成地形、镜头和基础色板,并运行给出实际截图。
后续逐步加入资产、水体光学和动物,沿用已有空间接口。
之后增加水体时,可以继续要求:“读取水体与接触部分,先实现波浪和反射,并保留分层查看功能。”增加海豚时,则沿用已有水面采样,将动作阶段、接触事件与水花连接起来。这样我们每次都在同一套场景结构上推进,也能单独修改某个模块。