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。此时
128 个 X 方向分段用来表达波浪轮廓;像素多不能弥补顶点不足。若波长
2. 时间和空间频率
A 是局部长度,k 是每局部长度的弧度,omega 是每秒弧度。保持相位恒定可得波峰速度
示例状态:
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 中各自累加。
法线为:
它在波峰和波谷处恢复竖直,在两者之间倾斜。若高光相位和位移相位不一致,先检查频率、时间和符号,而不是调 roughness 掩盖问题。
4. 增加第二组波
对
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 扩展为
5. 材质外观与水体边界
示例使用 Standard、蓝绿色、较低粗糙度,足够观察波面高光。真实水面往往还需要环境贴图或反射方案;把材质设为透明只会改变覆盖混合,不等于物理折射。Physical transmission 又涉及厚度、背景可见性和额外渲染成本,应作为独立需求实现。
DoubleSide 让背面可见,但水面投影在地面上只是验证阴影链路,不意味着透明水体应投出与不透明板相同的阴影。生产场景需要明确水的阴影近似方式。
水面同时开启 castShadow 与 receiveShadow,默认阴影设置可能产生自阴影条纹(shadow acne)。示例收紧方向光 shadow camera 的 near/far,并使用很小的 normalBias 与 bias。normalBias 沿法线偏移接收阴影的采样位置,bias 调整深度比较;它们不是法线公式的修复工具。数值过大会让阴影脱离物体或漏光,应根据场景尺度、光线夹角、阴影分辨率和深度范围调整,不能无条件复制到所有场景。
验证与练习
振幅设 0,应恢复静止平面;暂停时间,检查轮廓和地面阴影;提高频率,观察网格采样不足时的折线;临时关闭 normal 补丁,与正确高光对比。最后移到画面边缘检查扩展边界是否有效。
- 加入第二个沿 Z 方向传播的波,同步位置、法线及边界。
- 让波向正 X 传播,解释相位符号与传播速度的关系。
- 改用 PointLight,验证距离材质仍使阴影与波面一致。
- 放置一个 CPU 高度函数驱动的小浮标,讨论它与真正浮力模拟的区别。