Ocean Shader II: Fresnel, Industry Methods & FFT
每个像素射出一条光线。相机放在水面上方 3.5 个单位,随时间向前移动。射线方向由像素在屏幕上的位置决定——屏幕中心朝正前方,边缘向外散开。
用三个欧拉角生成旋转矩阵,角度随时间用 sin 轻微摆动,整个画面像在船上轻轻晃。这是给海面镜头增加临场感的常用技巧。
第一项光照是漫反射,取决于法线和光线方向的夹角:
表面越正对光越亮。Seascape 在标准公式上做了改动——让背光一面也保留一点亮度,模拟水体透出的柔和光(水是半透明的,背面不会全黑)。
把视线沿法线反射出去,看反射线有多接近光源:
越接近光源方向,高光越强。指数 shininess 控制光点大小——指数越大光点越小越锐利。这就是水面上那粒耀眼的阳光。
反射的强弱取决于视角:正对着看反射弱,掠射角看反射强。
水的 。掠射角()时 ,反射率趋近 100%。这就是为什么远处水面映出天空。
这里和 smallpt2/B15 玻璃折射用的是同一个菲涅尔原理——只是 smallpt 用它分配反射/折射能量,这里用它混合水面颜色。
水面颜色来自两部分:
| 来源 | 做法 |
|---|---|
| 反射(天空给的) | 视线沿法线反射,查那个方向的天空颜色 getSkyColor |
| 折射(水体自己的) | 深蓝底色 + 一点漫反射水色 |
一个是天空给的,一个是水体自己的。两者的混合比例由菲涅尔系数(B07)决定。
vec3 getSeaColor(vec3 p, vec3 n, vec3 l, vec3 eye) {
vec3 seaColor = ...; // 深蓝底色 + 漫反射
vec3 reflColor = getSkyColor(reflect(eye, n)); // 反射天空
float fresnel = ...; // 菲涅尔系数
vec3 color = mix(reflColor, seaColor, fresnel); // 按菲涅尔混合
color += waveHighlight(p); // 浪峰泛亮(随距离衰减)
color += specular; // 高光
return color;
}
海浪有大量又细又密的边缘,直接渲染会锯齿闪烁。办法:每个像素采样 9 次(略微错开的位置),取平均。这是超采样抗锯齿(SSAA)。
对最终颜色取 次方:
指数小于 1 会提亮整体、增加对比度,让画面更舒服。这是一种简化的色调映射(tonemapping)。
一道波就是一条移动的正弦曲线,叠好几道方向、波长不同的波。Gerstner 波的关键区别:顶点会横向移动,于是浪峰变尖、浪谷变圆,更像真浪。
控制浪峰尖锐度。
业界追求真正逼真的标准做法,源自 Tessendorf 的经典论文(《泰坦尼克号》年代就在用)。
真实海浪服从一个频谱(风把能量分布到各种波长和方向):
FFT 抓住了真实海面的统计规律——成千上万的波分量叠加,远比 Gerstner 几道波丰富。UE5 的水系统、大量 3A 大作都用它。
光有水面形状不够,可信的水靠这些细节:
| 技术 | 效果 |
|---|---|
| 泡沫(foam) | 浪太陡或近岸时泛白 |
| 屏幕空间反射(SSR) | 水面映出真实场景,不只是天空 |
| 焦散(caustics) | 阳光穿过水面在海底投出光斑 |
| 次表面散射(SSS) | 光透过浪尖透出通透的绿 |
| 水下吸收/雾 | 深度越大颜色越衰减 |
| 船浪相互作用 | 物体与水面的动态交互 |
这些细节叠加,才让水从”几何”变成”活的水”。