27 · 水面波动:位置、法线与阴影一起修改

返回系列目录

本章实现一块有几何起伏、正确坡面光照和同步投影的水面。它是水面形变的基础,不包含真实水体反射、折射、吸收或泡沫模拟。先把一条波的所有数据对齐,比直接堆叠复杂噪声更容易验证。

完整场景:water-waves.js。共享补丁:deformation.js。示例有 DirectionalLight、接影地面、动态时间和边界扩展,可按 README 方法运行。

1. 几何准备必须与公式一致

const geometry = new THREE.PlaneGeometry(6, 6, 128, 32);
geometry.rotateX(-Math.PI / 2);

PlaneGeometry 初始位于 XY 平面,烘焙旋转后局部数据位于 XZ,normal 朝 +Y。此时 y=h(x,z)y=h(x,z) 才和代码对应。只旋转 Mesh 不会改变 position attribute 的轴含义。

128 个 X 方向分段用来表达波浪轮廓;像素多不能弥补顶点不足。若波长 λ=2π/k\lambda=2\pi/k,每个波长要有足够分段才能平滑表达,实际数量取决于观察距离和轮廓要求,而不只是达到最低采样条件。

2. 时间和空间频率

h(x,t)=Asin⁡(kx+ωt) h(x,t)=A\sin(kx+\omega t)

A 是局部长度,k 是每局部长度的弧度,omega 是每秒弧度。保持相位恒定可得波峰速度 dx/dt=−ω/kdx/dt=-\omega/k:当前加号让波向负 X 传播。想反向传播可以改变时间项符号,而不是用画面直觉猜。

示例状态:

const state = {
  uDemoTime: { value: 0 },
  uDemoAmplitude: { value: 0.35 },
  uDemoFrequency: { value: 2 },
  uDemoSpeed: { value: 1.6 }
};
installDeformation(material, state, 'water');

uDemoSpeed 在此是角频率,不是每秒世界距离。修改 Mesh.scale 也会改变世界波长和位移幅度。

3. 三条链路共同使用一个函数

顶点位置使用:

vec3 demoDeform(vec3 p) {
  p.y += uDemoAmplitude * sin(uDemoFrequency * p.x + uDemoSpeed * uDemoTime);
  return p;
}

主材质在 begin_vertex 后执行它,法线在 beginnormal_vertex 后使用同一相位求导。深度和距离材质同样执行 demoDeform,但不重复安装主光照用的法线代码。onFrame 只更新一次共享时间,不在三个 pass 中各自累加。

法线为:

n=normalize⁡(−Akcos⁡(kx+ωt),1,0) n=\operatorname{normalize}(-Ak\cos(kx+\omega t),1,0)

它在波峰和波谷处恢复竖直,在两者之间倾斜。若高光相位和位移相位不一致,先检查频率、时间和符号,而不是调 roughness 掩盖问题。

4. 增加第二组波

对 h=Asin⁡(kx+ωt)+Bsin⁡(qz+νt)h=A\sin(kx+\omega t)+B\sin(qz+\nu t),同时修改共享位移函数与解析法线:

float demoDx = A * k * cos(k * position.x + omega * time);
float demoDz = B * q * cos(q * position.z + nu * time);
objectNormal = normalize(vec3(-demoDx, 1.0, -demoDz));

这段数学示意中的 A、B 等必须对应实际声明的 uniform 或常量后才可编译。不能只给位置加第二组波。包围盒的最大 Y 偏移也应从 A 扩展为 ∣A∣+∣B∣|A|+|B|。更复杂的 Gerstner 波还带水平位移,不能沿用纯高度场法线而不重新推导。

5. 材质外观与水体边界

示例使用 Standard、蓝绿色、较低粗糙度,足够观察波面高光。真实水面往往还需要环境贴图或反射方案;把材质设为透明只会改变覆盖混合,不等于物理折射。Physical transmission 又涉及厚度、背景可见性和额外渲染成本,应作为独立需求实现。

DoubleSide 让背面可见,但水面投影在地面上只是验证阴影链路,不意味着透明水体应投出与不透明板相同的阴影。生产场景需要明确水的阴影近似方式。

水面同时开启 castShadow 与 receiveShadow,默认阴影设置可能产生自阴影条纹(shadow acne)。示例收紧方向光 shadow camera 的 near/far,并使用很小的 normalBias 与 bias。normalBias 沿法线偏移接收阴影的采样位置,bias 调整深度比较;它们不是法线公式的修复工具。数值过大会让阴影脱离物体或漏光,应根据场景尺度、光线夹角、阴影分辨率和深度范围调整,不能无条件复制到所有场景。

验证与练习

振幅设 0,应恢复静止平面;暂停时间,检查轮廓和地面阴影;提高频率,观察网格采样不足时的折线;临时关闭 normal 补丁,与正确高光对比。最后移到画面边缘检查扩展边界是否有效。

  1. 加入第二个沿 Z 方向传播的波,同步位置、法线及边界。
  2. 让波向正 X 传播,解释相位符号与传播速度的关系。
  3. 改用 PointLight,验证距离材质仍使阴影与波面一致。
  4. 放置一个 CPU 高度函数驱动的小浮标,讨论它与真正浮力模拟的区别。