ShaderLab:海面——顶点变换,程序化生成无需贴图

一个基于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\max时返回1,中间平滑过渡。_FoamThreshold控制泡沫出现的位置,_FoamSoftness控制过渡柔和度 color = lerp(color, _FoamColor.rgb, foam),按foam因子,把海水颜色向泡沫颜色混合 color = MixFog(saturate(color), input.fogFactor),按雾因子与场景雾色混合

相关推荐
AIminminHu4 天前
((AI篇)OpenGL渲染与几何内核那点事-(二-1-(10):从“搜个大概”到“读懂图纸”:一个 CAD 开发者眼中的 RAG 进化简史)
人工智能·agent·opengl·智能体
雪域迷影6 天前
Windows上使用VS2026和CMake编译LearnOpenGL项目源代码
windows·cmake·opengl·vs2026·gthub
UTwelve7 天前
【UE】Gerstner Waves 水体模拟 4 :[颜色构成阶段3、4] - 实现NAP+CDOM
ue5·着色器
Yasin Chen7 天前
Unity TMP_SDF 分析(五)片元着色器
unity·游戏引擎·着色器
AIminminHu9 天前
OpenGL渲染与几何内核那点事-项目实践理论补充(一-3-(10):从“像素画师”到“硅基神明”:一个CAD开发者穿越GPU着色器管线的十年进化史)
着色器·片段着色器·顶点着色器·opengl 1.0·顶点/片段着色器
郝学胜-神的一滴11 天前
[简化版 Games 101] 计算机图形学 05:二维变换下
c++·unity·图形渲染·three.js·opengl·unreal
AIminminHu12 天前
OpenGL渲染与几何内核那点事-项目实践理论补充(一-3-(7):从“显卡不听话”到“GPU秒懂你”:一个CAD老兵的着色器驯服史))
着色器·编译流程·着色器语言 glsl·创建着色器对象·glcreateshader·gluseprogram·glcreateprogram
♡すぎ♡12 天前
ShaderLab:线条几何体旋转
unity·计算机图形学·着色器·shaderlab
AIminminHu13 天前
OpenGL渲染与几何内核那点事-项目实践理论补充(一-3-(4):你敢说你真的懂OpenGL?一个老师傅眼中的“图形API进化史”)
渲染·opengl·渲染管线