/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
WebGL 着色器中的水波纹与流体扰动模拟

在传统山水画与古典美学中,“水”是极具灵性与变幻的意象。春水微澜、秋水澄澈、落雨荡漪,水面的折射、反射与微弱波光为整幅画卷注入了无尽的生机。
在 WebGL / 3D 图形学中,如果只是给平面贴上一张静态的水面贴图,水面会显得毫无灵魂、如同一块僵硬的塑料板。而如果使用基于粒子或高度场的 Navier-Stokes 流体方程进行 CPU 物理求解,算力开销又极为巨大,无法在网页端实时运行。
借助正弦波叠加(Gerstner Waves)、法线贴图扰动(Normal Map Perturbation)与视差折射计算,我们完全可以在片元着色器中以极低的 GPU 算力模拟出如水墨荡漾般细腻灵动的水体效果。
Gerstner 波浪顶点位移数学模型
简单的正弦波($y = A \\sin(k x – \\omega t)$)只会产生上下对称的平缓起伏,无法表现真实波浪“浪峰陡峭、浪谷平缓”的非线性物理特征。
**Gerstner 波(格斯特纳波)**通过在垂直位移的同时引入水平方向的微量收缩,完美还原了真实海浪的尖顶形态:
$$\\begin{aligned}x' &= x – \\sum Q_i A_i D_{i,x} \\sin(\\vec{D}i \\cdot \\vec{P} \\cdot \\omega_i + \\phi_i t) \\z' &= z – \\sum Q_i A_i D{i,z} \\sin(\\vec{D}_i \\cdot \\vec{P} \\cdot \\omega_i + \\phi_i t) \\y' &= \\sum A_i \\cos(\\vec{D}_i \\cdot \\vec{P} \\cdot \\omega_i + \\phi_i t)\\end{aligned}$$
其中 $A_i$ 为波幅,$Q_i$ 为波峰陡峭度,$\\vec{D}_i$ 为传播方向向量,$\\omega_i$ 为角频率。
flowchart LR
Vertex[平面网格顶点] –> GerstnerCalc[顶点着色器: 4 组 Gerstner 波合成位移]
GerstnerCalc –> NewPos[生成起伏波峰与动态顶点法线]
NewPos –> FragShader[片元着色器: 法线贴图双向流动采样]
FragShader –> Fresnel[计算视线与法线夹角 (菲涅尔反射)]
Fresnel –> Refract[后处理 FBO 折射采样 + 天空盒反射混合]
Refract –> ScreenWater[输出波光粼粼的水面]
片元着色器法线扰动与菲涅尔水墨交融 GLSL 实现
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform sampler2D u_normalMap; // 水面微观法线扰动贴图
uniform sampler2D u_backgroundTex; // 经过 FBO 离屏渲染的池底/水下背景纹理
varying vec2 vUv;
varying vec3 vWorldPosition;
varying vec3 vNormal;
void main() {
// 1. 双向流动 UV:两层法线贴图以不同速度和方向错峰滚动
vec2 flowUv1 = vUv * 4.0 + vec2(u_time * 0.03, u_time * 0.02);
vec2 flowUv2 = vUv * 6.0 – vec2(u_time * 0.02, u_time * 0.04);
vec3 normal1 = texture2D(u_normalMap, flowUv1).rgb * 2.0 – 1.0;
vec3 normal2 = texture2D(u_normalMap, flowUv2).rgb * 2.0 – 1.0;
vec3 blendedNormal = normalize(normal1 + normal2 + vNormal);
// 2. 水面视线向量与菲涅尔系数(Fresnel Effect)
vec3 viewDir = normalize(cameraPosition – vWorldPosition);
float fresnel = dot(viewDir, blendedNormal);
fresnel = clamp(1.0 – fresnel, 0.0, 1.0);
fresnel = pow(fresnel, 3.0); // 菲涅尔指数增强
// 3. 水底折射采样:利用法线对屏幕 UV 进行微量位移扰动
vec2 screenUv = gl_FragCoord.xy / u_resolution.xy;
vec2 distortUv = screenUv + blendedNormal.xy * 0.03;
vec3 underwaterColor = texture2D(u_backgroundTex, distortUv).rgb;
// 4. 东方水墨水色调色板
vec3 waterDeepColor = vec3(0.08, 0.16, 0.19); // 墨蓝深水
vec3 waterShallowColor = vec3(0.23, 0.38, 0.35); // 苍翠浅水
vec3 sunGlowColor = vec3(0.95, 0.85, 0.55); // 潋滟金光高光
// 5. 镜面高光反射 (Specular Sun Light)
vec3 lightDir = normalize(vec3(0.5, 0.8, 0.3));
vec3 halfVector = normalize(lightDir + viewDir);
float specular = pow(max(0.0, dot(blendedNormal, halfVector)), 128.0);
// 6. 最终颜色多层合成
vec3 waterBase = mix(waterShallowColor, waterDeepColor, 0.6);
vec3 finalColor = mix(underwaterColor * waterBase, waterBase, 0.4);
finalColor = mix(finalColor, vec3(0.9, 0.95, 1.0), fresnel * 0.8); // 反射天空
finalColor += sunGlowColor * specular * 1.5; // 叠加波光高光
gl_FragColor = vec4(finalColor, 0.88);
}
交互式涟漪波(Interactive Ripples)拓展
当用户鼠标在 Canvas 上划过或点击时,我们可以通过 Ping-Pong 帧缓冲区在 GPU 上迭代波动方程(2D Wave Equation):$$\\frac{\\partial^2 h}{\\partial t^2} = c^2 \\nabla^2 h – \\gamma \\frac{\\partial h}{\\partial t}$$
- 鼠标点击处在高度图(Height Map)上注入一个高斯能量点;
- 波动在相邻像素间扩散开去并随阻尼衰减,产生如雨滴落入砚池般栩栩如生的水波涟漪。
用数学与着色器驾驭流体之美,让冰冷的像素在屏幕上荡漾出诗意盎然的东方水光。


