10 波浪的原理与实现
前置知识:第 2 章的浮点运算、第 3 章的顶点与片元阶段,以及第 5 章的软边。
本章示例:波浪线与顶点位移,完整源码。
学完本章能做什么
你将能从正弦函数构造空间中的周期波、随时间移动的行波,并分清“在片元里画一条弯线”与“真的移动网格顶点”。
1. 从一个振动点到一条波浪
sin(phase) 在 -1.0~1.0 之间连续变化。若只输入时间,每个位置同步上下振动:
float y = uAmplitude * sin( uTime );
要让不同的 x 对应不同的高度,把 x 放进相位:
y(x, t) = A × sin(2π × N × x - ω × t + φ)
| 量 | 含义 | 增大时的变化 |
|---|---|---|
A |
振幅 | 波峰和波谷离中线更远 |
N |
在 x ∈ [0, 1] 内的周期数 |
波峰更密;波长为 1 / N |
ω |
时间相位速度,弧度/秒 | 向右传播更快;真正的 x 方向速度为 ω / (2πN) |
φ |
初相位,弧度 | 整条波在初始时刻平移 |
固定一个波峰的相位不变:2πNx - ωt = 常数。随时间增大,x 必须增大,所以减号表示向 +x 传播;改成加号则向 -x 传播。ω 不是“每秒移动的 UV 距离”,这一点很容易混淆。
示例把 uTime 设置为 clock.getElapsedTime(),单位为秒;shader 中使用:
// 顶点或片元阶段;x 是 0~1 的水平 UV。
const float TAU = 6.28318530718;
float wave( float x ) {
return uAmplitude * sin( TAU * uCycles * x - uSpeed * uTime );
}
2. 片元阶段:画一条波浪线
片元阶段拿到 vUv,计算当前 x 对应的波高,再判断本片元离这条曲线有多远:
// 片元阶段;高度与厚度均为 UV 的 y 单位。
float centerY = 0.5 + wave( vUv.x );
float verticalDistance = abs( vUv.y - centerY );
float line = 1.0 - smoothstep( 0.008, 0.014, verticalDistance );
这是竖直距离,在坡度很陡的地方,看起来可能比平坦处粗。若要近似恒定的垂直于曲线的厚度,可以把距离除以 sqrt(1.0 + slope * slope),其中 slope = A * TAU * N * cos(phase);这建立在 x、y 单位和屏幕比例已经统一的前提下。入门示例选择竖直距离,便于看清正弦函数的作用。
因为每个片元独立求值,即使平面只有 1 × 1 个分段,线也能弯曲。提高几何分段数不会提高这条片元绘制的波浪线的形状精度。
3. 顶点阶段:让平面真的弯曲
要让网格轮廓或顶点位置改变,就在顶点着色器中计算同一条波:
// 顶点阶段;position 是平面局部坐标,偏移量也是局部 y 单位。
vec3 displaced = position;
displaced.y += wave( uv.x );
gl_Position = projectionMatrix * modelViewMatrix * vec4( displaced, 1.0 );
此时 uAmplitude 改变的是局部几何单位,和前一节的 UV y 单位只有在你明确建立映射后才可比较。示例平面高度为 1.2;在片元模式中使用 UV 位移,在几何模式中使用局部位移,同一个滑块的数值只是方便对比,并非同一屏幕距离。
GPU 只移动已有顶点,三角形内部依然是直线插值。因此 PlaneGeometry(2, 1.2, 1, 1) 几乎看不到波浪轮廓;把横向分段提高到 64 或更多才会逐渐接近正弦曲线。波数越多,所需分段越多。比如 N = 5、横向只有 10 段,每周期只有约两个采样点,形状容易严重失真。示例的辅助线对应网格分段位置;它们是片元着色器画出的参考线,网格是否弯曲仍取决于顶点位置。
4. 多条波怎样组合
两个波可以相加:
float height = 0.12 * sin( TAU * 2.0 * x - 1.4 * uTime ) +
0.035 * sin( TAU * 7.0 * x + 2.3 * uTime );
大波决定整体起伏,小波增加细节。不同传播方向和周期会产生时而增强、时而抵消的干涉。若小波的频率增加,顶点位移需要更多网格分段;若只在片元中画线,主要限制变成像素采样和抗锯齿。多波相加的最大可能振幅是各振幅绝对值之和,应给画面边缘留出空间。
5. 波浪、法线与光照
示例使用不依赖法线的 ShaderMaterial 颜色,方便只观察形状。若未来把波浪用于受光照的水面,移动顶点后原来的 normal 不再对应新表面;不能只改 gl_Position 就期待光照自动正确。第 15 章会专门讲位移后的几何法线,第 16 章讲只在片元制造细小明暗纹理的法线。
常见问题
- **波浪不动:**检查渲染循环是否更新
uTime,以及renderer.render()是否持续调用。 - **方向相反:**检查相位里的时间项是减号还是加号。
- **顶点位移像折线或完全看不见:**检查几何体横向分段数与周期数;同时确认不是只在片元阶段改变颜色。
- 动画太快:
uSpeed是弧度/秒;一个完整周期需要2π / uSpeed秒。 - **振幅很大时被裁掉:**扩大网格周围的可视区域或减小振幅。
本章小结
波浪来自随 x 和时间变化的相位。片元模式计算每个像素到曲线的距离;顶点模式改变真实几何,受分段数限制。相位符号决定传播方向,振幅、周期数与时间相位速度分别控制不同性质。