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<min时返回0,当x>max时返回1,中间平滑过渡。_FoamThreshold控制泡沫出现的位置,_FoamSoftness控制过渡柔和度

color = lerp(color, _FoamColor.rgb, foam),按foam因子,把海水颜色向泡沫颜色混合

color = MixFog(saturate(color), input.fogFactor),按雾因子与场景雾色混合

相关推荐
CAD开发小慧2 天前
CAE软件开发:Coreform为何用ACIS处理CAD几何?
算法·架构·计算机图形学
ly_Enhs2 天前
从曲面细分到计算着色器:我的 Vulkan 实验记录
着色器·vulkan图形渲染c/c++
扶尔魔ocy3 天前
【qt openGL入门笔记】OSG实现飞机模型在网格图上飞行并进行态势标绘(四)
qt·opengl·osg
:mnong4 天前
Web 三维可视化技术对比:ParaView Web vs 本项目 wasm + Three.js
前端·webgl·opengl·opencl·科学可视化
刻BITTER4 天前
LocalLow 乱码文件排查 Intel INSC着色器缓存文件
缓存·哈希算法·着色器
扶尔魔ocy5 天前
【qt openGL入门笔记】OSG实现显示飞机模型飞行的点迹(二)
qt·opengl·osg
扶尔魔ocy5 天前
【qt openGL入门笔记】OSG实现飞机模型在网格图上飞行(三)
opengl·osg
扶尔魔ocy6 天前
【qt openGL入门笔记】OSG实现飞机模型在网格图上飞行
qt·opengl·osg
扶尔魔ocy10 天前
【qt openGL入门笔记】FPS相机
qt·opengl
扶尔魔ocy10 天前
qt openGL入门笔记】FPS相机和轨道相机
qt·opengl