# AI 生成 3D 场景拆解 01：Tideglass Cove

规范网页：https://devtookit.com/article/14

作者：Revio

发布时间：2026-10-10T05:39:26Z

更新时间：2026-10-10T05:39:26Z

摘要：学习 Techartist 的 Tideglass Cove，按地形、岩石、植物、房车、水体、动物、光照和镜头拆解程序化实现与关键公式，并将共享空间规则、建模方法和动效经验整理为可直接调用的 Skill。

---

![Tideglass Cove 日间全景，原作者在线 Demo 的运行截图](/articles/tideglass-cove/cove-day.jpg)

[在本站体验 Demo](/demos/tideglass-cove) · [原作者 GitHub 仓库](https://github.com/iamtechartist/tideglass-cove)

Tideglass Cove 是一个可以交互的风格化海湾。棕榈、房车和石拱围绕沙滩展开，前方的水体剖面让我们能直接看到海草、鱼群和鳐鱼。拖动可以旋转场景，滚轮可以缩放；还能切换黄昏、调整潮位、点击水面制造涟漪、打开房车门，或让两只海豚跃出水面。

本篇参考学习的 Demo 来自 **Techartist（GitHub 用户 iamtechartist）**，原地址为 [github.com/iamtechartist/tideglass-cove](https://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 说明](https://github.com/mrdoob/three.js/wiki/Three.js-Shading-Language) 与 [WebGPURenderer 文档](https://threejs.org/docs/pages/WebGPURenderer.html)。

### 程序化生成

场景资产全部由程序生成。这个版本没有加载 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 | 碰撞包络、合批、镜头、交互与质量控制 |

[入口与初始化源码](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/main.js#L49-L65)

## 地形

地形是其他模块的基础。沙滩与海底共用一张连续曲面，植物落脚、水面接触和动物避障都要采样它的高度。

### 海岸线

作者先用 `shore(x)` 描述海岸位置，再用 `d = z - shore(x)` 判断一个点处于海岸的哪一侧。

海岸曲线是一个抛物线加两组正弦扰动：

```text
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 个场景单位的正弦起伏，形成沙纹和轻微不平整。

把这些项放在一起，高度函数就有了清楚的分工：

```text
地形高度 = 岸坡过渡 - 深水降低 + 沙丘 - 凹地 + 小幅沙纹
高斯隆起 = A × exp(-((x-x₀)²/a + (z-z₀)²/b))
```

高斯函数适合调整一个局部区域：中心附近影响大，向外自然衰减，不容易出现硬边。营地则有另一种处理：用两个平滑遮罩相乘得到矩形区域，再把其中的高度混合到 0.87，让房车附近的地面比较平整。石拱下方通过椭圆遮罩 `grottoBasin()` 接入浅盆地，避免拱脚和海底完全脱节。

这里反复使用的平滑函数是三次 Hermite 插值：

```text
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 层径向网格封闭，并在外圈复用侧壁落点。

细节也围绕这个剖面服务：侧壁有少量岩石碎片，主体埋在断面内部，只露出断裂尖端；前方切面保持干净，方便观察海底与生物。岩层材质沿高度叠加正弦色带与细窄接缝，读起来像有沉积层次的石灰岩。

水的侧面沿同一圈地形边界生成，上沿顶点跟随水面，下方接到局部海底；着色器继续剔除低于地形或高于水面的片元。上表面与侧面共享水面函数，因而浪动时不会一个在动、另一个停在原地。

从全景看，前方切面同时露出岩层、水和鱼，让海湾内部也成为画面的一部分。

[海岸与高度函数](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/core.js#L9-L27) · [轮廓与网格生成](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/terrain.js#L5-L14)

## 岩石

这里有两类需求：碎石需要简单而有变化的轮廓，石拱需要连通的岩体和空腔。它们采用了不同的建模方法。

### 碎石

小岩石由九边形截面搭起来。每个角的半径加入两组正弦扰动，再沿四个高度层缩放和偏移：底部较收、腰部鼓起、顶部进一步收窄，顶面也带倾斜。索引网格转成非索引后重新计算法线，让不同三角面保留棱面感。

岩石位置有两类：靠岸的大石块使用明确坐标，作为构图与接触对象；水下石架周围的碎石使用有种子的局部随机分布。贝壳则沿潮线以上的窄带生成小型扇形网格，避免把整个沙滩均匀撒满。

### 石拱

石拱需要左右支柱、顶部岩盖和内部空腔连成一个整体，作者在 `archField(x,y,z)` 中定义一个**类似有符号距离场的隐式标量场**，再提取零等值面。通常把负值看作岩体内部、正值看作外部；这里包含位移和多种组合，不保证数值处处都是严格的欧氏距离。

这种写法的基本运算很容易理解：

```text
并集         min(A,B)
交集         max(A,B)
从 A 挖去 B  max(A,-B)
```

项目用多个平面不等式的 `max()` 定义左右支柱、顶板和后壁，再通过平滑最小值 `smin()` 把它们连接起来：

```text
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**。源码把每个采样立方体分成六个四面体，检查四面体各条边的两端是否异号；若异号，就按线性插值找到零点：

```text
t = f(A)/(f(A)-f(B))
P = A + t(B-A)
normal ≈ normalize(∇f)
```

三个交点组成一个三角形，四个交点则排序后拆成两个。法线通过中心差分近似场的梯度。当前石拱传入的采样步长是 0.066；它决定网格细节与构造成本。渲染时仍然是普通三角网格，项目没有用逐像素 SDF 光线步进渲染石拱。

[岩石与石拱构造](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/terrain.js#L20-L103) · [等值面提取](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/sculpt.js#L3-L19)

## 植物

### 棕榈树干

五棵棕榈的参数分别规定位置、高度、倾斜、叶长和叶柄数量。树干中心线沿高度参数 t 弯曲，水平偏移主要用 t²，所以根部稳定，越接近树冠偏移越明显。

树干沿中心线生成 100 段、每圈 14 份的截面，半径逐渐收细；根部加粗，高频正弦塑造环状生长痕，截面的角向扰动打破圆柱感。树根用短曲线管连接到真实地形高度。

### 叶片

棕榈叶由叶轴和两侧的小叶组成。先为羽状叶建立弯曲的主轴，中心线包含水平伸展、向上抬升、末端下垂和少量侧向卷曲。再沿主轴两边长出一对对小叶。小叶的长度遵循 `sin(πt)` 的幂函数，中段宽，根部与尖端窄；横截面中间略高、两侧略低，形成折叠而不是完全平的纸片。

作者还加入了几种叶片变化：少量小叶缺失，左右位置略错开，叶长与扭转不同；最后三片大叶被设为较短、较直立的年轻叶，旧叶的部分末端混入少量黄褐色。树冠的叶基、椰果、叶柄也有对应几何，避免叶片像悬在树干旁边。

### 风动

叶片生成以后，还要让它们随风摆动，并保持叶根与叶轴相连。源码把运动分为三个层次：

| 运动层 | 控制量 | 作用 |
| --- | --- | --- |
| 树干整体偏移 | 主体权重 t² | 根部不动，树冠随低频风摆 |
| 叶柄弯曲 | 沿叶轴的权重 t² | 叶柄根部跟随树冠，自由端摆动更大 |
| 小叶颤动 | 小叶长度权重 q² | 小叶根部保持连接，叶尖有细微抖动 |

每个顶点还携带 `windAnchor` 四维属性和 `windWeight` 三维属性，记录它所属棵树的锚点、相位、叶方向，以及上述三种运动的权重。所有相连部分共享整体位移，只有自由端叠加更局部的变化。

我们复用这套风动时，需要保留这些锚点和权重，否则各部分独立摆动容易拉开接缝。动画通过 `material.positionNode` 在 GPU 上执行，没有骨骼、蒙皮和外部风动画贴图。

### 地被与海草

地被采用相似思路：贴地匍匐茎按地形高度取点，小叶通过叶柄接到茎上；海草则用细长条带，每个顶点存储 `flex = t²`，底部固定、顶端摆动。棕榈与遮阳篷受共享的 Breeze 参数控制，海草使用独立的低幅度周期运动。

[植物网格与风动源码](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/plants.js#L4-L76)

## 房车与营地

![房车与营地近景 展示窗框 轮拱 遮阳篷 椅子和灯笼](/articles/tideglass-cove/camp-door.jpg)

### 车身

房车侧面先用 `Shape` 描出圆润的车顶、车头和底边，轮拱直接进入轮廓，窗与门用 `holes` 开孔，再由 `ExtrudeGeometry` 挤出薄壳，并给边缘加小倒角。窗框使用带内孔的另一张形状，玻璃单独放在孔内。

车顶沿侧面共有的贝塞尔边界展开一张曲面，再在横向加入轻微的正弦拱起。斜前窗同样先生成二维形状，再把顶点映射到 `frontPoint(z,v)` 定义的倾斜曲面上，让前窗框、玻璃与车身共享形状关系。

其余零件各取适合的方法：圆环生成轮胎和方向盘，圆柱生成轴、支杆和灯；RoundedBoxGeometry 生成内饰、座椅、柜体、后视镜与台阶。冲浪板沿长度生成逐渐收尖的截面，再抬起两端；折叠椅的四个落脚点分别采样地形高度。

### 材质

车漆颜色由材质函数生成。材质用世界高度减去车身基准高度，再通过一条很窄的 `smoothstep` 把下部陶土红和上部奶油色连接起来；微弱的高频噪声补一点油漆表面变化。窗玻璃使用 Physical NodeMaterial，`transmission` 为 0，透明由 0.43 的 opacity 实现。它让我们能看到内部，光学效果比后面单独实现的水体简单。

### 开门

门的局部坐标原点放在竖向铰链上，旋转门组即可开关，目标角度约为 `0.56π`。门角使用指数平滑靠近目标，房车内部保留座椅、柜子、桌板、方向盘和灯光，让开门后有内容可看。

### 遮阳篷

遮阳篷的下垂与细小褶皱写入初始网格，顶点柔性权重取 `sin(πu)sin(πv)`：边缘权重为零，中间布面才能随风上下起伏。条纹通过局部 x 坐标上的正弦函数形成，高频噪声负责细微织物感。

[房车与营地源码](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/camper.js#L5-L102)

## 水体

水体牵涉几何、光学和接触效果，也是这个场景中依赖关系最多的模块。我们按水面网格、波浪、反射、折射和岸边效果的顺序来看。

### 水面网格

水面几何先使用更细的地形网格，逐三角形裁切到地形高度不超过 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 |

```text
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 检查沙地接触、海豚入水和水滴落回水面的时刻。我们让画面与接触检测共用这些参数，就能让海豚和水滴落在实际显示的水面上。

水面法线由波高梯度构造：

```text
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 型指数吸收：

```text
T = exp(-σL)
σ = (0.78, 0.13, 0.079)
Cwater = Cbehind × T + Ctint × (1-T) × 0.55 × 深度修饰
```

红通道吸收系数最大，因此水后面的颜色随距离增加更容易失去红色，转向青蓝。这里的系数与颜色按场景风格选择，不能直接当成真实海水测量值。

表面反射与透射通过一个 Schlick 风格的视角权重混合：

```text
F = 0.023 + 0.85(1-max(n·v,0.02))⁵
C = mix(Cwater, Creflection, F) + 太阳高光
```

俯视时更容易看见海底，贴近水面看时反射增强。这个式子借用了 Fresnel 的形状，但 0.85 是美术调节项，并非严格的 `1-F₀`。太阳高光则对镜面方向与视线的点积使用 650 次幂和 90 次幂，组合出很窄的亮点与较宽的弱光泽。

[水面与光学源码](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/water.js#L5-L100) · [共享波动参数与函数](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/core.js#L29-L59)

### 泡沫

泡沫由三项相加：岸边泡沫、岩石周围泡沫，以及浅水里的细小浪脊。它们都需要浪的到达相位和噪声遮罩，因而不会变成一条静止的纯白描边。

岸边部分先计算 `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)` 比较，算出当前浸湿程度，再与衰减后的上一帧湿度取较大值：

```text
wetNew = max(currentContact, wetOld × exp(-Δt/11))
```

11 秒是指数衰减的时间常数，不表示到第 11 秒突然完全变干。反复被浪触碰的地方会继续保持湿润，刚退水的位置则逐渐恢复干沙。材质把暖浅色干沙混到较深的湿沙，粗糙度从约 0.98 降到 0.30；完全淹水后另用约 0.87 的粗糙度，并加入焦散颜色修饰。

### 焦散

![低机位水体剖面 海底 鱼群 海草和投影可以同时观察](/articles/tideglass-cove/water-section.jpg)

焦散指水下常见的流动亮纹。项目用程序噪声近似这些图案：`mx_noise_float` 在这份 Three.js vendor 源码中映射到 Perlin 噪声。

做法是沿太阳方向把世界位置投影到一个二维坐标，再加低频时间偏移与噪声扭曲。两组不同尺度、不同时间方向的噪声相加，寻找它们接近零的窄带，形成细亮纹；另一项高斯乘积制造小型亮斑。低频 focus 遮罩控制哪里更集中，避免全海底密密麻麻地亮起来。

亮度还受入水深度、表面朝向、石拱遮蔽和昼夜控制：深处指数衰减，朝向太阳的面更明显，洞下和黄昏更弱。深度越大，图案尺度也改变，让效果有空间层次。

石拱内部又作了专门处理。材质用局部坐标遮罩选中洞内面，稍微偏向青蓝，并把类似焦散的动态图案加到一项很弱的 emissive 上；同时辅以一个小点光源。这是为洞内可读性增加的反射光暗示，没有实时求解完整的间接光照。

[湿度与材质函数](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/core.js#L61-L113) · [岩石接触纹理](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/terrain.js#L105-L112)

### 涟漪

交互涟漪最多保留八个事件，每个事件只存中心 x、z，发生时间和强度。事件循环使用槽位，点击很多次时新事件覆盖旧事件，不会无限增加计算量。

令 r 为片元到事件中心的距离，a 为事件年龄，涟漪高度可写成：

```text
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 的邻居平均速度对齐、邻居中心聚合三规则。

### 避障

环境碰撞以简单几何包络为主：海底与水面构成竖向限制，岩石用扩大后的椭球，海豚用沿身体方向的胶囊，场景边界则取最近边上的安全距离。若点在椭球内部，代码把它投影回膨胀后的表面：

```text
q = sqrt((dx/rx)² + (dy/ry)² + (dz/rz)²)
q < 1 时 P' = center + (P-center)/q
```

这是行为避让用的保守包络，包含可见岩石以外的余量，并非逐三角网格碰撞或通用刚体系统。对于固定海湾和少量动物，它足以减少穿石、穿底与互相重叠。

[鱼群与鳐鱼源码](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/life.js#L6-L54) · [碰撞与投影修正](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/collision.js#L4-L35)

## 海豚

### 建模

作者沿海豚身体轴列出十个采样站，每站给定横向半径、纵向半径和截面中心高度，再使用**单调三次 Hermite 插值**连接它们。

在斜率同号时，节点切线使用相邻斜率的加权调和平均；斜率反号时设为零。这个形状保持方法可以减少普通曲线插值过冲，避免细嘴、颈部或尾柄附近出现异常鼓包。主体用 84 个纵向环、每环 32 份的椭圆截面生成，背鳍、胸鳍与尾鳍再由形状挤出或参数曲面补齐。

材质按局部高度混合浅腹部和深背部，鳍区再变深，粗糙度也不同。尾部运动依然由顶点位移实现：越靠近尾端，纵向权重越大；跳跃的不同阶段还改变摆尾强度。

### 跳跃

一次跳跃包含起跳前的准备、空中飞行和入水后的恢复。每只海豚都有下面的状态顺序：

```text
巡游 cruise → 集合 gather → 接近 approach → 加速 power
→ 飞行 flight → 入水 entry → 恢复 recovery → 巡游
```

水下阶段通过三次贝塞尔曲线移动。项目给定起点、终点和两端速度，再转换成控制点：

```text
B₀ = P₀
B₁ = P₀ + V₀T/3
B₂ = P₁ - V₁T/3
B₃ = P₁
```

这使前后阶段可以保持速度方向的连续性，减少路径连接处突然拐弯。双海豚手动跳跃使用不同的水道和相反方向，并在完成后换到另一侧的巡游区域。

飞行阶段则用重力抛物线：

```text
x(t) = x₀ + vx t
y(t) = y₀ + vy t - 0.5 × 9.81 × t²
```

水下路径与空中弹道分别计算，再通过状态机衔接。身体朝向由速度向量建立局部坐标基，因此飞行上升时头朝上、下降时头朝下。

### 入水

入水检测取的是沿速度方向伸出的前端接触点，而非海豚中心。下降时，如果这个点低于 CPU 水面函数，就在最近一个时间步内做 16 次二分，找到更准确的接触时刻，再发出水花和涟漪。

海豚更新采用不大于 1/90 秒的子步，减少快动作漏过水面的问题。默认约第 12 秒开始一次自动跳跃，随后间隔约 28 秒轮换；手动双跳会延后下一次自动动作。Jump 按钮在动作进行时禁用，避免重入状态机。

[海豚截面与跳跃源码](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/dolphins.js#L7-L85)

## 水花

海豚碰水后触发两层效果。第一层是最多 480 个小球实例组成的水滴池，每个完整强度喷溅发出约 180 滴；第二层是向外扩展、向上抬起再塌下的冠状水膜，保留四个独立槽位，让两只海豚的事件可以同时存在。

### 水滴

每滴水记录初始位置、速度、延迟、大小和阻力系数。初始方向不是完全均匀的球形爆炸，还叠加海豚水平运动方向，形成沿入水方向偏移的喷溅。

水滴使用线性阻力与重力方程的解析解。设阻力系数 k、时间 a、重力 g，令 `travel = (1-exp(-ka))/k`，则：

```text
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 秒内衰减。

这两层效果分别表现喷出的细水滴和短暂撑开的水膜，都由入水事件触发。它们使用明确的弹道、形变与回收规则，适合这种短促的展示效果。

[水花粒子与水膜源码](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/splash.js#L3-L26)

## 风格与光照

### 构图

这片海湾的风格化主要体现在体积、比例和色彩的取舍。棕榈构成上方轮廓，奶油色与陶土红的房车提供视觉焦点，右侧石拱平衡营地，青绿水体铺在前景，有限厚度的岩层把所有内容收成一个小型景观标本。

我们从画面看到的这些关系，在源码中对应固定布局、五棵高低不同的棕榈、前方切面和受控色板。植物与海豚都有细密几何，风格化主要来自整体造型与配色。

### 材质细节

表面细节按尺度分工。岩石用低频噪声调色，中频正弦做层理，高频噪声产生孔隙和凹凸；沙地用更细的噪声与小幅沙纹。大量变化的幅度只在几个百分点以内，使大色块和轮廓仍然清楚。`bumpMap()` 主要改变光照法线，几何轮廓仍由真正的网格承担。

棕榈叶按小叶与横截面分配顶点色，再在背光时加入很弱的绿色发光，暗示叶片透光。这是一种简单的透光近似。石拱内部、水下焦散、灯笼与鱼尾亮点也都使用局部控制，避免整片场景一起泛亮。

### 环境光

光照包含强度 3.05 的暖方向光、半球补光、较弱的冷色方向光，以及约 0.25 的环境强度。环境反射图是运行时生成的 **256 × 128 浮点纹理**：天空、地平线与地面颜色渐变，加一个高斯太阳亮斑，再由 PMREM 预过滤，以便不同粗糙度的材质读取不同宽度的反射。

背景另用 `screenUV.y` 生成稳定的画面渐变。最终使用 ACES Filmic 色调映射，日间曝光约 1.03。项目没有配置景深、SSAO、Bloom、轮廓描边或体积雾后处理。

### 黄昏

![黄昏海湾 冷色环境与房车和灯笼的暖光](/articles/tideglass-cove/cove-dusk.jpg)

Dusk 在日间与黄昏两套预设之间过渡。主方向光从暖色逐渐变冷，强度降到约 0.26；半球光转为蓝紫，侧向冷光稍微增强，环境纹理混到黄昏版本，曝光微调到 1.10。

房车内部灯和灯笼随黄昏参数增强，灯泡几何的 emissive 也提高；石拱内部点光源稍强，并叠加低频呼吸般的变化。水体颜色、泡沫和亮点同时转入另一套色彩，使场景各部分仍然协调。

昼夜、风和门角的过渡使用同一类指数平滑：

```text
valueNew = valueOld + (target-valueOld) × (1-exp(-kΔt))
```

这样参数变化按秒控制速度，帧率波动时也能保持相近的响应。相较于每帧固定插值百分比，它更适合不同设备上的连续交互。

[环境图与配色](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/main.js#L23-L24) · [灯光配置与昼夜更新](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/main.js#L50-L77)

### 接触阴影

房车下面有独立的短距离接触阴影，按底盘与四个轮胎分别布置透明网格，把各顶点投到沙地高度。透明度按 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 毫秒。

[相机预设与窄屏处理](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/main.js#L18-L33) · [OrbitControls 设置](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/main.js#L58-L59)

## 性能优化

这个场景有大量小叶和密集的石拱网格，水体又需要额外的离屏渲染。性能优化主要围绕合批、纹理尺寸、刷新频率和像素比展开。

### 合批

作者使用了三种合并方式。植物在生成阶段按树干、叶片和叶柄分别合并；普通静态对象按材质、阴影标志、拾取角色与图层分桶，烘焙变换后合并；动物则在自身坐标中合并固定零件，保留会动的尾巴等部件；房车门轴也保持独立。透明材质、依赖顶点位移的材质和动态父节点不能随意加入普通静态合批。

合批还需要检查材质是否依赖局部坐标。石拱内部材质设置 `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` 可用于固定场景状态。

当前结构仍有可以继续研究的地方：屏幕空间光学缺少屏外信息，水面法线采用局部近似，顶点风动与动物变形没有完整重建所有变形后的法线，鱼群使用成对检查，初始化还需要构建石拱网格并编译材质。后续扩大场景规模时，可以分别研究光学稳定性、法线修正、空间分桶和资产缓存。

[合批规则](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/optimize.js#L3-L12) · [质量设置与动画循环](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/js/main.js#L31-L80)

## 学习总结

这个项目最值得我们带走的，是各模块如何共用一套规则。海岸同时影响地形、波幅和泡沫；水面同时参与画面、湿沙和动物入水；植物的连接关系又决定风动时哪些顶点必须一起移动。后续自己做场景时，可以先把这些关系建立起来，再增加资产与细节。

### 学到的经验

这里的房车、石拱和五棵棕榈都有明确的位置与比例。随机数主要补充叶片、岩面和水花的变化。我们复用时也可以先确定主体和镜头，再用带种子的局部变化丰富表面。

高度场适合连续陆海地形，截面适合石块和动物身体，带孔挤出适合房车，隐式场适合石拱。为每个部分选择合适的表示，比给所有对象套同一种生成算法更容易控制结果。

树干与叶片共享风动锚点，车顶与侧面共享曲线边界，水面与侧面共享波函数。这些接口决定细节增加以后，场景是否仍然连贯。

湿沙要记住刚才是否被浪触碰；海豚入水要给出准确的位置、方向和时刻，再触发涟漪与水花。只读取当前一帧的位置，往往不够表达这些变化。

全景看布局，营地近景看部件连接，低机位看水体剖面。反射、折射和泡沫还能分别输出，帮助我们找到具体哪一层需要调整。

### 可复用的 Skill

我们把这些方法整理为 **procedural-3d-scene**：主文件负责场景生成流程，案例文件保存 Tideglass Cove 的算法、公式、适用条件和源码位置。以后分析新的 Demo，可以继续添加案例，而不用把某个项目的尺寸和配色固定成通用规则。

[下载完整 Skill](/articles/tideglass-cove/procedural-3d-scene.zip) · [查看 Skill 正文](/articles/tideglass-cove/procedural-3d-scene/SKILL.md) · [查看 Tideglass Cove 案例](/articles/tideglass-cove/procedural-3d-scene/references/tideglass-cove.md)

这个项目归纳进 Skill 的内容如下：

| Skill 部分 | 从 Tideglass Cove 学到的方法 | 后续可以用在哪里 |
| --- | --- | --- |
| 构图与参数 | 固定主体布局、窄屏机位、有种子的细节 | 小岛、庭院、营地等展示场景 |
| 共享空间接口 | 连续地形，CPU/GPU 共用水面参数 | 落脚、避障、接触与材质一致性 |
| 资产生成 | 截面、曲线挤出、隐式场、叶轴与小叶 | 按形状选择程序化建模方法 |
| 材质与光照 | 分尺度纹理、程序环境图、昼夜联动 | 统一风格，保持整体配色 |
| 水体与接触 | 分层光学、岩脚距离图、湿度历史、事件涟漪 | 有岸线和独立岩石的小水域 |
| 动画与行为 | 分层风动、阶段运动、端点速度、入水事件 | 植物、游动动物、跳跃与喷溅 |
| 合批与画面检查 | 保留局部坐标、动态部件，调节离屏渲染 | 控制大量小部件和水体的成本 |

### 如何交给 AI 使用

在 Codex 中，将解压后的 `procedural-3d-scene` 文件夹放进 `~/.codex/skills/`，就可以在任务中使用 `$procedural-3d-scene`。这里的 Skill 已经分开保存通用流程与案例细节，我们调用时只需要补充场景目标、技术栈和本次要完成的范围。

```text
使用 $procedural-3d-scene，制作一个可以环绕查看的风格化海湾。
参考 Skill 中的 Tideglass Cove 案例，使用 JavaScript、Three.js 和 TSL。
场景包含沙滩、植物、营地和浅水，不导入外部模型或图片贴图。
先用占位体完成全景与竖屏构图，再实现连续陆海高度场。
定义共享的 terrainHeight、waterHeight、time 和 tide 接口。
本次先完成地形、镜头和基础色板，并运行给出实际截图。
后续逐步加入资产、水体光学和动物，沿用已有空间接口。
```

之后增加水体时，可以继续要求：“读取水体与接触部分，先实现波浪和反射，并保留分层查看功能。”增加海豚时，则沿用已有水面采样，将动作阶段、接触事件与水花连接起来。这样我们每次都在同一套场景结构上推进，也能单独修改某个模块。

### 参考资料

- [Techartist 的 Tideglass Cove 原仓库](https://github.com/iamtechartist/tideglass-cove)
- [Tideglass Cove 本站 Demo](/demos/tideglass-cove)
- [本文分析的固定源码版本](https://github.com/iamtechartist/tideglass-cove/tree/17f7d97a8b62d8410e70951d51dae134da3e02a8)
- [原项目 MIT 许可证与版权声明](https://github.com/iamtechartist/tideglass-cove/blob/17f7d97a8b62d8410e70951d51dae134da3e02a8/LICENSE)
- [Three.js Shading Language 官方说明](https://github.com/mrdoob/three.js/wiki/Three.js-Shading-Language)
- [Three.js WebGPURenderer 官方文档](https://threejs.org/docs/pages/WebGPURenderer.html)
