ShaderLab:水面
水面表现是一个技术美术中一个常见的课题,在本篇文章中我参考了Alexander Alekseev在2014年创作的Seascape,并以此代码为基础,翻译成ShaderLab语言,并分析其中每个函数的作用和必要性。
ShaderLab代码:https://pan.baidu.com/s/1wuerhUVqQg8amo6a0rk2Ug?pwd=9527
源代码地址:https://www.shadertoy.com/view/Ms2SD1
效果预览

代码分析
全局变量与宏
NUM_STEPS = 32:射线与海平面求交时的迭代次数上限,参数越大越准,但是也越慢;PI:圆周率常量EPSILON = 1e-3:求交收敛阈值,小于这个误差就认为命中海面EPSILON_NRM (0.1 / iResolution.x):法线采样步长,随分辨率变化,避免分辨率越高法线越噪//#define AA:注释掉的抗锯齿开关;取消注释会启用 3x3 超采样
海面参数
ITER_GEOMETRY = 3:几何求交用较少 octave(快)ITER_FRAGMENT = 5:片元着色/法线用更多 octave(细节更丰富)SEA_HEIGHT = 0.6:海浪总振幅基准SEA_CHOPPY = 4.0:浪尖“尖锐程度”(越大越“碎浪”)SEA_SPEED = 0.8:海面动画速度SEA_FREQ = 0.16:波频率基准(控制波长)SEA_BASE:深水基色(偏深蓝)SEA_WATER_COLOR:水体散射色(偏青绿),再乘 0.6 降亮度SEA_TIME:时间相位,1 + iTime*speedoctave_m:2x2 变换矩阵,用于每层波形坐标扭转/缩放,打破重复感
fromEuler
用途:把相机射线方向按时间旋转,让镜头运动更有“飞行感”。
欧拉角到旋转矩阵,函数输入一个angle包含x/y/z三轴角,输出mat3旋转矩阵。分别计算angle.xyz三个分量的sin/cos缓存到vec2类型的a1、a2、a3。然后声明矩阵m,手动填充矩阵的每一行,旋转顺序为YXZ。
hash与noise
用途:给波形加入随机扰动,避免规则条纹。hash(vec2 p):二维伪随机函数,输入一个二维变量,输出[0, 1)之间的随机值。dot(p, vec2(127.1,311.7)):把2D坐标映射到标量。fract(sin(h)*43758.5453123):经典 hash 技巧,返回 [0,1) 随机值。noise(vec2 p):二维噪声函数。i = floor(p):格点整数坐标。f = fract(p):格点内局部坐标。u = f*f*(3-2f):Hermite平滑曲线(平滑插值权重)。
-1.0+2.0*mix(mix(hash(i+vec2(0.0, 0.0)), hash(i+vec2(1.0, 0.0)), u.x), mix(hash(i+vec2(0.0, 1.0)), hash(i+vec2(1.0, 1.0)), u.x), u.y):四角双线性插值(先x后y),将二维点映射到[-1, 1]。
四角双线性插值即根据点在格子内的位置对四个角点的值做线性插值。
其中函数u = f*f*(3-2f)函数如下图蓝色线所示:
光照函数
用途:计算漫反射和镜面反射光照diffuse(n,l,p):自定义漫反射。pow(dot(n,l)*0.4+0.6, p):把 N·L 映射后再指数塑形,避免纯 Lambert 太灰。specular(n,l,e,s):镜面高光。nrm = (s+8)/(PI*8):Phong/Blinn 风格归一化系数近似,使光线满足能量守恒定律:反射光的总能量 ≤ 入射光的总能量的经验公式。pow(max(dot(reflect(e,n),l),0),s)*nrm:反射向量与光方向夹角决定高光强度。
天空颜色
用途:地平线更亮,天顶更深,形成自然天穹渐变。
getSkyColor(vec3 e):按视线方向e返回天空颜色。
e.y = (max(e.y, 0.0)*0.8 + 0.2)*0.8:只取朝上的部分并压缩范围。
r = pow(1.0-e.y, 2.0):红色通道值。
g = 1.0-e.y:绿色通道值。
b = 0.6+(1.0-e.y)*0.4:蓝色通道值。
color = vec3(r, g, b)*1.1:构造RGB渐变并*1.1增量。
其中函数color = vec3(r, g, b) * 1.1如下图所示:
sea_octave
用途:单层海浪,生成尖浪,choppy越大越尖。sea_octave(vec2 uv, float choppy):输入uv坐标与choppy尖锐度。uv += noise(uv):先做噪声扰动。wv = 1-abs(sin(uv)):生成波谷/波峰形状。swv = abs(cos(uv)):另一组互补波形。wv = mix(wv, swv, wv):非线性混合,让形状更自然。pow(1-pow(wv.x*wv.y,0.65), choppy):得到尖浪形状,choppy 越大越尖。
其中函数wv = mix(1-|sinx|, |cosx|, 1-|sinx|)
map
用途:粗略高度场,返回“点到海面”的符号距离近似,准确说是y与波高差。freq = SEA_FREQ,amp = SEA_HEIGHT,choppy = SEA_CHOPPY:初始化频率/振幅/尖锐度。uv=p.xz,并 uv.x*=0.75:x 方向拉伸,避免各向同性太假。h=0:累积波高。
循环 ITER_GEOMETRY 次(3 次)。h+=d*amp:累计波高+=当前层波高*振幅。
每层后:坐标乘 octave_m、频率乘 1.9、振幅乘 0.22(典型分形谱)。choppy 向 1.0 混合,越高频(ITER_GEOMETRY越大)越平滑。
返回 p.y - h:>0 在海面上,<0 在海面下。
map_detail
用途:高细节高度场,得到更细腻的波形,法线和最终着色阶段需要更高细节,求交阶段用粗略版提速。
逻辑同粗略高度场相同,但是循环次数是ITER_FRAGMENT(5 次)而不是 3 次。
getSeaColor
用途:计算海水颜色。
输入:点 p、法线 n、光方向 l、视线 eye、距离向量 dist。fresnel=clamp(1.0-dot(n, -eye), 0.0, 1.0):计算菲涅尔效应反射强度,并限制区间[0, 1]。fresnel=min(fresnel^3, 0.5):立方增强并上限 0.5:掠射角反射更强。reflected:反射天空色。refracted:折射/散射近似 = 水基础色 + 漫反射系数*水色*调制系数。color=mix(refracted, reflected, fresnel):用 Fresnel 在折射与反射间混合。atten=max(1.0-dot(dist,dist)*0.001, 0.0):随距离衰减(dot(dist,dist) 即距离平方)。color+=SEA_WATER_COLOR*(p.y-SEA_HEIGHT)*0.18*atten:加入浅水/波峰增亮项(与高度差和衰减相关)。color+=specular(n,l,eye,600.0*inversesqrt(dot(dist*dist)))加镜面高光,指数随距离自适应:远处更柔,近处更锐。
返回最终海水颜色。
getNormal
用途:法线估计
输入:点 p 与采样步长 eps。n.y = map_detailed(p):当前高度差。n.x = map(p+dx)-n.y:x 方向差分, dF/dx ≈ y(x + dx, z) - y(x, z)。n.z = map(p+dz)-n.y:z 方向差分, dF/dz ≈ y(x, z + dz) - y(x, z)。n.y = eps:把 y 分量设为步长,构造梯度向量, 因为dy = map(p+dy)-n.y。
返回归一化后得到的法线。
法线计算:
- F(x,y)=0: n = (dF/dx, dF/dy)
- F(x,y,z)=0: n = (dF/dx, dF/dy, dF/dz)
- z=F(x,y): n = (dF/dx, dF/dy, -1)
heightMapTracing
用途:计算射线与海面交点与起始位置的距离,以及交点位置。
输入:射线原点 ori、方向 dir;输出交点 p;返回参数 t。tm=0:近端参数。tx=1000:远端参数。hx=map(ori+dir*tx):远端高度差。
若远端仍在海面上方(hx>0),说明这条射线可能没打到海面,直接返回远端。hm=map(ori):近端高度差。
迭代最多 NUM_STEPS 次。tmid = mix(tm,tx, hm/(hm-hx)):按符号距离做割线插值,比二分更快收敛。
计算中点位置 p 和中点位置距离海面的高度hmid=map(p)。
根据 hmid 正负更新区间 [tm, tx](夹住零点)。abs(hmid)<EPSILON 收敛则提前退出。
返回一次插值后的最终 t。
getPixel
用途:渲染单个像素的颜色
输入:屏幕坐标与时间。uv = coord / iResolution:像素坐标归一化到 [0,1]。
将屏幕坐标映射到 [-1,1]。
按宽高比修正 x。ang:随时间变化的欧拉角,控制镜头摆动/旋转。ori:相机原点,y=3.5 在海面上方,z 随时间前进(穿行效果)。
构造初始视线,dir.z += length(uv)*0.14 让边缘略抬,形成镜头畸变感。
用 fromEuler(ang) 旋转射线。
调用 heightMapTracing 求海面交点 p。dist = p-ori 距离向量。
用距离自适应的 eps 计算法线(远处步长更大,抗噪)。
固定主光方向(上方偏前)。mix(天空, 海面, 权重):
dir.y决定看天空还是看海面;smoothstep(0,-0.02,dir.y)在地平线附近柔和过渡;- 再
pow(...,0.2)调曲线。
mainImage
ShaderToy 标准入口。time = iTime*0.3 + iMouse.x*0.01:时间缩放并允许鼠标 x 改变相位。#ifdef AA:若定义了 AA,走抗锯齿路径。color=0 初始化。
3x3 邻域采样并累加(9 次)。
平均得到抗锯齿颜色。
否则单采样 getPixel(fragCoord,time)。
post 注释:后处理阶段。pow(color, vec3(0.65)):近似 gamma/色调调整,让画面更通透。
输出 fragColor,alpha=1。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)