一个基于URP的程序化海面shader:顶点阶段把网格变换形成波浪,偏远阶段重新计算更精细的法线,再做漫反射、镜面、高光、菲涅尔、泡沫和雾效。
这篇文章是在上一篇文章水面的基础上,采用其基本方法延伸而来,本文章中的着色器不是一个适用于全屏效果的着色器,而是应用在一个平面上,通过更改平面顶点位置来模拟海浪效果。上一篇文章地址:
https://blog.csdn.net/qq_65445239/article/details/160600613?spm=1001.2014.3001.5501
代码分享:https://pan.baidu.com/s/1-j1dVEwnk5yNEwBBy5QlQA?pwd=9527
代码、风格参考:https://www.shadertoy.com/view/Ms2SD1
效果预览

代码分析
材质面板参数:
- _SeaScale: 海面的放缩因子,越小宽广的海面效果越好
- _DeepColor: 深水区基础颜色
- _ShallowColor: 浅水区基础颜色,比深水区更亮、更偏青绿色
- _FoamColor: 泡沫颜色,接近白色、略带蓝,波峰处会混入这个颜色
- _SpecColor0: 镜面高光颜色,默认纯白色,表示太阳高光反射
- _WaveHeight: 波浪高度振幅,值越大海面起伏越高
- _WaveFrequency: 波浪频率,默认值比较低
- _WaveSpeed: 波浪随时间推进的速度
- _WaveChoppiness: 波浪尖锐程度,越大波峰越尖锐,更有破碎海浪的质感
- _NormalStrength: 法线扰动强度,越大法线变化越剧烈,光照起伏越明显
- _NormalEpsilon: 采样法线时的偏移距离,太小数值不稳定,太大细节丢失
- _FresnelPower: 菲涅尔强度指数,越大边缘反射越明显,正视角反射更弱
- _Smoothness: 镜面高光幂次,传统的Blinn-Phong高光指数
- _FoamThreshold: 泡沫出现阈值,判断波峰是否足够高
- _FoamSoftness: 泡沫边缘柔和程度,越大泡沫过渡越柔和
- _FoamSteepness: 法线陡峭程度对泡沫的影响系数,越陡越容易生成泡沫
ShaderLab标签:
- "RenderPipeline"="UniversalPipeline": 说明这是给URP用的shader
- "RenderType"="Opaque": 表示不透明物体
- "Queue"="Geometry": 放在几何体队列渲染,一般是2000附近的不透明队列
Shader配置:
- LOD: Level Of Detail: shader复杂度等级,unity可根据显卡能力选择是否支持,300表示中高复杂度
- Cull Off: 背面剔除关闭
Pass配置:
- "LightMode"="UniversalForward": 告诉URP,这个Pass用于向前渲染主光照
- #pragma target 3.0: 要求shader model 3.0,老硬件可能不支持
- #pragma vertex vert: 指定顶点着色器入口函数叫vert
- #pragma fragment frag: 指定片元着色器入口函数叫frag
- #pragma multi_compile_fog: 生成带雾/不带雾等多个编译变体,ComputeFogFactor和MixFog要配合这个使用
- #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl",引入URP核心库,提供
- 变换函数
- _Time
- 雾效函数
- 空间坐标工具
- #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl": 引入URP光照库,提供GetMainLight()、SampleSH()、光照结构体Light
材质常量缓冲区
CBUFFER_START(UnityPerMaterial)
.......
CBUFFER_END
定义一个常量缓冲区
UnityPerMaterial:URP/Unity 约定的材质参数缓冲区名。
Properties 中的参数在这里映射为 HLSL 变量。
静态常量
- GEOMETRY_ITER: 顶点阶段波浪迭代次数,用于位移网格顶点
- FRAGMENT_ITER: 片元阶段迭代次数,用于法线和更高精度采样
- OCTAVE_M: 2x2矩阵每一层波形叠加后用这个矩阵变换uv,旋转+缩放矩阵
哈希函数
用途:把二维坐标伪随机映射到一个0~1随机数
float h = dot(p, float2(a , b)),把二维坐标p投影到一个固定向量上,得到一个标量h
return frac(sin(h) * 43758.5453123),先做非线性变换,然后放大制造更多小数变化,最后取小数部分,是经典的shader伪随机哈希写法
2D噪声函数
用途:输入二维坐标,输出平滑噪声值
float2 i = floor(p),向下取整,i得到当前点所在的网格整数坐标
float2 f = frac(p),取小数部分,f表示点在当前格子内部的位置,范围[0, 1)
float2 u = f*f*(3.0-2.0*f),平滑插值多项式,作用是让插值更平滑,避免格子边界突变
float a = hash21(i + float2(0.0, 0.0)),取左下角格点的伪随机值,同理b,c,d
return -1.0+2.0*lerp(lerp(a, b. u.x), lerp(c, d, u,x), u.y), 基于当前点在格子中的位置,对四个角点的随机值进行线性插值,然后映射到[-1, 1]
单层海浪波形函数
用途:一层海浪形状函数,返回该层波浪的高度贡献
uv += noise2(uv),用噪声扰动uv,让波纹不是机械规则的正弦波叠加,而更自然
float2 wv = 1.0 - abs(sin(uv)),分别对x、y分量做正弦,然后把其映射成一种"脊线"形状
float2 swv = abs(cos(uv)),分别对x、y分量做余弦,产生另一种波纹形状
wv = lerp(wv, swv, wv),让波形更复杂,不像纯正弦那么单调
return pow(1.0-pow(wv.x*wv.y, 0.65), choppy),将x,y两个方向的波纹合成,用1.0减去可以翻转波形,让波峰区域更明显,否则起伏就会过小,choppy参数用于控制波峰的尖锐程度
时间函数
用途:获取海浪动画时间值
return 1.0 + _Time.y * _WaveSpeed,返回结果用于推动uv随时间流动
采样波浪高度
用途:获取某世界位置当前时间的海面高度
uv.x *= 0.75,把x方向压缩到75%,相当于让波纹在x方向尺度不同,打破完全对称
float h = 0.0,累计高度的初始值
float t = seaTime(),当前海浪时间
unroll\],HLSL展开循环的编译提示,因为循环次数是常量,编译器能更好优化
for (int i = 0; i \< FRAGMENT_ITER; i++),循环计算海浪的累计高度
float d = seaOctave((uv+t)\*freq, choppy),计算当前层波的高度
d += seaOctave((uv-t)\*freq, choppy),再叠加一组反向时间推进的波,使波更复杂,不像单向平移
h += d\*amp,把当前层波浪贡献乘以当前振幅后累加到总高度
uv = mul(OCTAVE_M, uv),用矩阵变换uv,旋转并拉伸采样方向,使下一层波的方向和尺度不同
freq \*= 1.9,每层频率放大1.9被,越向上层波纹越细
amp \*= 0.22,每层振幅衰减到原来的0.22,细节层起伏越来越小
choppy = lerp(choppy, 1.0, 0.2),把choppy逐渐按指数迅速向1.0衰减,高层细节波不那么尖锐
return h,返回累计采样的波浪高度
#### 采样波浪法线
用途:通过差值采样计算法线,用于参与计算当前像素的颜色
float eps = max(_NormalEpsilon, 0.001),取采样偏移距离
float h = sampleWaveHeight(xz, FRAGMENT_ITER),当前点高度
float hx = sampleWaveHeight(xz + float2(eps, 0.0), FRAGMENT_ITER),x方向偏移一点后的高度
float hz = sampleWaveHeight(xz + float2(0.0, eps), FRAGMENT_ITER),z方向偏移一点后的高度
float dhdx = (hx - h) \* _NormalStrength,近似x方向高度导数,乘常数来增强法线的起伏
float dhdz = (hz - h) \* _NormalStrength,近似x方向高度导数
return normalize(float3(-dhdx, 1.0, -dhdz)),对高度场y=h(x,z),法线近似为n = (-dh/dx, 1, -dh/dz)
这里还可以通过叉积来近似计算法线,不过要考虑法线的正负
#### 天空反射颜色函数
用途:手动计算一个渐变颜色,来模拟真实天空盒,并进一步用于计算菲涅尔效应颜色
e.y = (max(e.y, 0.0) \* 0.8 + 0.2) \* 0.8,越靠近地平线值越低,同时负数截断
return float3(pow(1.0 - e.y, 2.0), 1.0 - e.y, 0.6 + (1.0 - e.y) \* 0.4) \* 1.1,简化的"天顶较蓝、地平线偏亮"的天空反射颜色
#### 顶点着色器
用途:计算每个顶点的世界坐标,并转变到裁剪空间输出,以及计算部分片元着色器所需的参数
Varyings output,声明输出结构体变量
float3 worldPos = TransformObjectToWorld(input.positionOS.xyz),将顶点坐标从物体空间转换到世界空间
worldPos.y += sampleWaveHeight(worldPos.xz, GEOMETRY_ITER),根据世界坐标xz采样波高,并把结果增加到顶点的世界坐标上,这一过程只循环4次,节约顶点开销
output.worldPos = worldPos,把位移后的坐标传给片段着色器
output.viewDir = GetWorldSpaceNormalizeViewDir(worldPos),计算从当前点指向相机的方向,并归一化,用于片元中的反射/菲涅尔
output.uv = input.uv,传递uv
output.positionCS = TransformWorldToHClip(worldPos),把世界空间位置转到齐次裁剪空间,HClip = Homogeneous Clip Space
output.fogFactor = ComputeFogFactor(output.positionCS.z),根据裁剪空间z计算雾因子,在片元着色器中混雾
return output,返回数据给片元着色器
#### 片元着色器
float3 normalWS = sampleWaveNormal(input.worldPos.xz),采样计算海面法线,重新根据更多迭代次数的高度场计算,更加精细
float3 viewDirWS = normalize(input.viewDir),归一化视线方向
Light mainLight = GetMainLight(),从URP获取主光源
float3 lightDirWS = normalize(mainLight.direction),取主光源方向并归一化
float3 halfDirWS = normalize(lightDirWS + ViewDirWS),半角向量,用于Blinn-Phong高光,法线与半角向量越接近,高光越强
float waveHeight = sampleWaveHeight(input.worldPos.xz, FRAGMENT_ITER),重新采样一次当前波高,如果采样步数一致则可以从顶点着色器中传递过来,否则就需要重新计算
float normalizedHeight = saturate(waveHeight / max(_WaveHeight \* 2.4, 0.001)),归一化波高到大概0\~1范围,其中2.4是经验系数,用max(..., 0.001)防止除零
float crest = normalizedHeight + (1.0 - normalWS.y) \* _FoamSteepness,波峰/泡沫判定值,由两部分组成:normalizedHeight,越高越像波峰;(1.0 - normalWS.y) \* _FoamSteepness,法线y越小,说明表面越陡峭,越容易形成泡沫,由_FoamSteepness赋予权重
float3 ambient = SampleSH(normalWS),用球谐函数采样环境光,根据法线方向获取环境漫反射颜色,其数据来自unity环境光/天空盒烘培信息
float ndl = saturate(dot(normalWS, lightDirWS)),法线和光线方向点积,表示高光烦色强度,ndl表示normal dot lightDir
float ndv = saturate(dot(normalWS, viewDirWS)),法线和视线点积,越小表示越斜着看水面,ndv表示normal dot viewDir
float fresnel = pow(1.0 - ndv, _FresnelPower),简化版菲涅耳,斜角观察(看远处)时fresnel更大反射更强
float specular = pow(saturate(dot(normalWS, halfDirWS)), max(_Smoothness, 1.0)),Blinn-Phong高光
float3 waterBase = lerp(_DeepColor.rgb, _ShallowColor.rgb, saturate(normalizedHeight \* 0.7 + (1.0-normalWS.y) \* 0.3)),对深水颜色和潜水颜色进行线性插值,插值因子:normalizedHeight \* 0.7,波越高颜色越偏浅色;normalWS.y \* 0.3,越朝上的面越偏深色。最终waterBase就是海水的基础色。
float3 reflected = getSkyColor(reflect(-viewDirWS, normalWS)),计算视线方向与当前法线方向的反射方向,再通过这个反射方向获取自制天空盒颜色
float3 refracted = waterBase \* (0.2 + 0.8 \* ndl),不是严格折射,是一个透过水看到水体本色的近似,当前像素的法线更加朝向主光源方向的时候更亮
float3 color = ambient \* waterBase,初始颜色 = 环境光 \* 水基础色
color += lerp(refracted, reflected, fresnel),在折射与反射之间按菲涅耳插值,看远处时fresnel更大反射更多,看近处时fresnel更小折射更多
color += _SpecColor0.rgb \* specular \* mainLight.color,叠加主光源的高光颜色,_SpecColor0.rgb高光颜色,specular高光强度,mainLight.color主光源颜色
float foam = smoothstep(_FoamThreshold - _FoamSoftness, _FoamThreshold + _FoamSoftness, crest),smoothstep(min, max, x),当x\