当前位置: 首页 > news >正文

Shader - 水体(保姆级)

最终效果预览



水体的深度

水的深度 : 越靠近斜坡的地方深度越浅,颜色越淡越透.反之则相反
利用深度值计算的话,要先开启深度值

在shader中加入深度纹理的声明,屏幕坐标的采样UV

我们把采样深度图的纹理叫做depthTex,把采样深度纹理的UV叫做screenUV,用screenUV采样深度纹理.
在下图中,screenUV的含义是利用屏幕空间下像素坐标除以屏幕的宽和高的像素值(裁剪空间的坐标经过除以w分量齐次化之后又乘以了屏幕像素变换到了屏幕坐标中,除以 _ScreenParams.xy是为了做归一化的处理)


此时的depthTex不是线性的深度值,原因是



渲染水面时,必须在 Shader 标签里设置Queue="Transparent",并且关闭深度写入(通常是ZWrite Off)。
原理解析:渲染引擎画东西是有顺序的。如果你把水面当成“不透明的砖块”画出来,它就会把自己的深度记录在“深度图”里,把水底下的石头完全挡住(覆盖)。把水设置成半透明且不写入深度,是为了给水底的物体让路


因为水面没有写入自己的深度,所以此时全局的深度图(_CameraDepthTexture)里,干干净净地只保存了场景里那些不透明物体(比如河床、石头、岸边泥土)离摄像机有多远。
代码对应:就是你在 Shader 里写的SAMPLE_TEXTURE2D(_CameraDepthTexture, ...),你采样出来的这个值,其实是水底石头的深度,而不是水的深度


现在我们有了水底石头的深度,那“水面”本身的深度去哪找呢?因为水面没进深度图,所以我们只能在代码里“当场计算”。
原理解析:我们可以把水面面片的顶点坐标,通过矩阵乘法转换到“观察空间”(以摄像机为原点的空间)。此时,它的 Z 轴数值(Z值),就是此时此刻水面这个像素距离摄像机的物理距离。

拿刚才得到的两个深度值做减法,就能算出水的实际深度。
原理解析:*公式:水的绝对深度 = 水底石头距离摄像机的深度 - 水面距离摄像机的深度
如果两个深度相减,结果接近于 0:说明水面和石头几乎挨在一起,这里就是交界处(岸边或露出水面的礁石),可以在这里画白色的泡沫。
如果两个深度相减,结果非常大:说明石头离水面很远,这里是深水区,应该渲染成深蓝色,并且水花要变弱。

在subshader中写入下面的语句:

因为要在frag中利用vert计算好传过来的观察空间下的值,所以在varying中声明:

在vert中将模型空间的顶点转换到观察空间中:

水的绝对深度 = 水底石头距离摄像机的深度 - 水面距离摄像机的深度。利用该公式在frag中写上:
返回waterDepth查看效果


水的颜色


定义两个颜色,一个颜色用于靠近物体的边缘部分,一个用于控制大面积的白色部分


在CBUFFER_START中声明对应的变量:

在frag中利用lerp函数进行颜色的差值,再返回查看效果

水的泡沫

因为水的泡沫相对独立一些,仅依靠深度值,再引入一些纹理和参数就可以调节水的泡沫了,所以先写水的泡沫部分.水的泡沫部分采用采样纹理的方法来表现





调整纹理的缩放大小,得到下图的效果

在frag中写下方逻辑,并返回查看效果


现在要控制waterDepth的范围,声明一个范围参数(0~2)来控制它,如果该参数小于1,那么白色范围就会减少,对应浅滩面积增大,反之则相反
为了减少文章长度,在properties中声明的参数和在SRPBATCHER中的参数非必要不再书写
在frag中加入此range为0~2的数值,返回之后查看效果


再返回FAndWCol查看效果

为了给水面增加流动效果,可以利用_Time.y对采样的uv进行偏移,再叠加一个速度
在vert中写下方逻辑即可,此时的uv就流动起来了

为了让泡沫呈现出具体的形状

在frag中写下面的逻辑,再返回查看效果

可以看到因为step的函数的原因,因为waterdepth远离浅滩的值几乎都为1,所以foamtex和它对比后返回的值都是0,即黑色

通过调整foamtex的缩放可以改变水花的形状

为了避免后续出现问题,将foamtex的通道改为单通道来匹配waterdepth,同时取出采样出来的四维通道中的一个通道,在这里rgb都可以


将foamcolor和watercolor叠加后输出,注意变量的之间的修改

现在的foamcolor有一个问题,当模型被拉伸时,它也会被跟着拉伸


为了解决这个问题,将引入世界空间下的坐标来采样纹理



"传统模型 UV 记录的是相对比例(0 到 1)。当模型被拉伸时,UV 空间的映射范围被强行拉扯,从而导致纹理产生明显的拉伸与模糊。

而世界空间平面映射采用的是绝对物理尺寸(真实的米)。模型拉伸时,世界坐标并不会发生形变,仅仅是使更大范围的世界坐标参与到了当前像素的采样计算中,因此纹理的物理大小和分辨率始终保持稳定。"




但是这样引出了一个新的问题,水花不会流动了,因为原来的流动是在uv上的,世界坐标并没有流动
所以要在世界坐标上添加流动,将uv的名字修改为foamUV,代表这个UV是专门用来采样水花纹理的UV,再叠加上世界空间顶点坐标
在frag中将i.positionWS.xz修改为i.foamUV

但是问题又来了,我想让它随着我改变tilling的值而改变,但是又不想它因为模型的缩放而变化,怎么办?顺着这个问题,如果让tilling *世界空间的顶点会怎么样?


这样问题就解决了
在调试时发现了一个bug,当拖动waterdepth的时候,水花会和水深一起移动,所以waterdepth *= _WaterDepth是错误的,应该新建一个变量来接收这个值


添加下面的一句代码,作用是:利用幂函数调整泡沫噪声纹理的对比度,进而控制水面泡沫的密集程度(稀疏度)和边缘硬度。 其中_FoamArea是新增的range变量

解析:




水面颜色BUG调整


刚刚调试又发现一个bug,调整水面颜色的时候出现了乱七八糟的情况,这种情况是因为插值因子出现了问题,我们的插值因子是waterDepth,它的来源是:
float waterDepth = linearDepth + i.positionVS.z;
这个值并不是0到1的值,而是0到很大的值,所以它作为插值因子的时候,计算公式是这样的:


总之,原因是waterdepth的值太大
解决办法:我们让它限定在0到1的范围,并且让它除以一个自定义的变量值


水下的扭曲

为了能够看到水下的物体,首先水体要设置为透明水体,在subshader中写下
Blend SrcAlpha OneMinusSrcAlpha
并添加如下代码:

即可看到水下的物体,没有阴影是因为忘记打开平行光了

如果想要实现水下扭曲的方法,可通过采样屏幕抓取纹理再扭曲屏幕抓取纹理
也可以使用这种方式:总共采样两张纹理,一张纹理采样过后当作另一张采样纹理的UV坐标,就是把A纹理采样出来的纹理值A当作uv坐标,去采样纹理B,这样就达到了一个扭曲的效果(有省略);
打开如下设置

将渲染队列设置为透明队列

加入抓屏纹理的声明和寄值器的声明
我的老天,发现了一个问题,我忘记说之前采样的foamTex其实是一张噪声纹理了.OMG
将采样的噪声纹理的uv(foamTex)和screenUV进行插值,插值因子是_WaterDistort,这是一个新声明的范围0到1的变量,当它为0时,lerp函数计算出的就是foamTex的值,并赋值给distortUV
再用distortUV作为抓屏纹理的采样UV值,就实现了扭动的效果

return一下看看效果,后续还会对该效果进行改进,现在的问题是,foamTex的tilling是需要调整的,调整它的同时也会影响到扭动效果,所以需要单独用一张纹理的纹理值对抓屏纹理采样

这里采用一张法线纹理贴图

在frag中写下面的逻辑

由于需要经常用到_Time.y*Speed,所以将它用一个speed变量储存起来,以后直接用speed即可
在vert中对waterDisUV进行缩放和旋转的应用之后加上速度,这样法线纹理就可以产生效果了


在frag中写下面的逻辑,将watercolor * opaqueTex的目的是混合抓屏纹理和水面的颜色,此时颜色值可能会出现过曝的情况,为了解决这种情况可以将waterColor * 0.5来解决,或者加上saturate,或者通过乘法而不是加法的方式混合颜色(指的是fianlcolor和opaqueTex)


现在的效果

观察下图,会发现虽然物体进行了扭曲,但这个扭曲就好像是复制了一个物体进行扭曲,因为它不仅扭
曲了,还保持物体原有的形状,这是为什么?

通过寻找发现,是watercolor的问题,因为opaqueTex没毛病,foamcolor也没毛病,那只能是watercolor的问题了,先确定下来,因为后面要通过C#脚本写渐变的颜色替代watercolor;
现在的主要问题是,物体不仅在水下进行扭曲,在水上也进行了扭曲,因为之前是对抓屏纹理的整个纹理进行了扭曲
但是这张抓屏纹理背后是有物体的,这个物体不会扭曲,而且显现了出来


深度图重建扭曲效果

我们先对深度图进行扭曲看看效果
在frag中写下面的逻辑,用前面的lerp函数计算出的distortUV对深度图进行采样

得到的效果是

发现不仅下面的相交处产生了扭曲,上方的边缘也产生了扭曲,这是为什么?
其实原因出在

也就是说深度值的相加不仅发生在模型的表面,还发生在看不见的区域
那么让小于0的扭曲深度值 = 之前不扭曲的深度值,在深度图中水面之上的物体就不会进行扭曲了
有了这个值之后,把它作为抓屏纹理的采样UV,水面之上的物体就不会进行扭曲了
但是抓屏纹理的采样UV是可以在screenUV和DisNormal中切换的,所以也应该对深度值进行插值
在frag中写下面的逻辑
让opaqueUV = 扰动的UV,如果深度图中的值小于0,对应的就是水面之上的物体,那么就让它等于screenUV


但是if语句开销大,我们换成别的函数来代替

亦或是用三目运算符


水面的高光
加入以下变量


注意对向量进行归一化.返回之后查看效果

但我们想要的高光其实是波光粼粼的感觉,前面在对水下扭曲时,它的效果是这样的

扭曲高光


能否利用这张法线纹理对高光进行偏移呢?可以试一试,将N替换为DisNormal

得到的效果,发现是可以的

虽然水面的效果是波光粼粼的,但是现在你会发现,这种效果的流向是单一的,这是不对的,应该是不确定的流向,我们要让法线的流动从单一流动变为对着互相流动,即需要两个流动的值
此时之前定义的float4 waterDisUV里的zw分量就派上了用场

在frag中写下面的逻辑,让两个法线相乘,返回查看效果


将DisNormal作为高光里的法线项和半程向量点积

返回查看效果


将高光和水面进行叠加,一般来说如果想把高光叠加,都是采取加法的操作


水的反射

水的反射是不能做实时反射的,因为开销很大
应该采用使用反射向量采样cubeMap的方法来实现.
我们要自己先准备好一张cubeMap,声明该cubeMap纹理并计算出反射向量采样cubeMap
由于我们的法线向量已经是经过扰动后的向量了,用它计算出来的烦反射向量五花八门,跟漫反射似的,所以没有太大意义
办法是用一个lerp函数在原始法线和扰动法线之间插值,插值因子是新声明的_Reflection
计算出反射向量后,代入采样函数中,返回CubeMap中观察效果


菲涅耳

水是有很强的菲涅耳效应的,当视线与水体呈掠视时,看到的就是很强的波光粼粼的效果(反射效果强)
当视线垂直入射水体时,看到的就是清澈的水体(反射效果弱)
在frag中写下面的逻辑,其中_FresnelScale是新声明的float变量,返回fresnel查看效果



我们想要的是垂直入射时是黑色的效果,也就是水波不可见,掠视时是白色的效果,也就是可见
用1减去原式进行效果反转

返回查看效果



将反射乘在高光上,返回并查看效果


水的焦散

采样一张纹理,让纹理值贴通过该面片贴在面片底下的物体上
这里需要用到深度贴花的原理来制作的水的焦散效果
首先需要获取到贴着地表的一个坐标
在frag中写下面的逻辑
首先构建一个深度值,该深度值的值是从摄像机到该顶点的距离
让该深度值的w值等于1,利用三角形相似的性质求出该深度值的xy
让深度值的z值 = lineardepth(之前求得的线性深度值)
返回查看效果


先采样纹理,输出查看一下,采样纹理的时候用的uv坐标是世界空间下的刚刚构建的深度值depthWS的xz值,为什么是xz值?,因为我们只想让它在xoz平面上(o是原点)流动,不需要在y轴上进行偏移

查看效果
刚刚声明了transform_Tex,所以现在可以控制这张纹理的tilling来缩放纹理的大小
现在来调整纹理的流动
修改后的代码

查看效果,但是这个流动的截图出来也是静止的,所以这里就不放图了
只要你的纹理不是流动的,那就是错误的
现在和之前水面的流动是同一个问题,焦散的流动是单一的,比较死板,我们要让它随机的流动起来

是时候学习unity的UGUI了,明日再更












http://www.jsqmd.com/news/907346/

相关文章:

  • CentOS环境下手动升级openssl、openssh
  • MacType字体渲染引擎深度解析:Windows字体美化的核心技术方案
  • AVL Cruise 2023 保姆级教程:手把手教你用自带实例模型搞定纯电动车续航仿真
  • RTX51 Tiny在SiLABS SFR分页机制下的移植优化
  • RTX51 Tiny调试技巧与C源代码显示问题解析
  • 在mac上安装hermes
  • 鼎捷Tiptop ERP 5.3版本下,手把手教你用SoapUI测试一个用户登录WebService接口
  • RAG 技术体系:从向量检索到生产级 Pipeline
  • 保姆级教程:用PyTorch Geometric搭建GCN,实战DEAP脑电情绪分类(附完整代码)
  • 深入UGUI底层:手把手教你用OnPopulateMesh和顶点偏移,实现Image的任意变形(不只是倾斜)
  • 大数据处理:Spark与分布式计算
  • 用 Nerfstudio 和手机照片,5分钟快速生成你的第一个 3D 数字手办(Nerfacto 模型实战)
  • 告别双系统安装噩梦:Intel RST模式下无损切换AHCI,保住Windows再装Ubuntu
  • 论文降AI率工具怎么选?2026年4款降AI软件实测一次选对
  • 从零开发游戏需要学习的c#模块,第二十九章(经验值与升级系统)
  • 从一次“幻觉”到一次“进化”:AI事实核查错误的深度剖析与系统改进启示
  • 从状态检查到数据备份:仓储PLC控制器保养周期与实操清单
  • 效率拉满!VS Code 安装 Qoder CN(原通义灵码)详细教程
  • MySQL—隔离级别和MVCC
  • Docker 网络进阶:容器间通信与 DNS 解析
  • 百度网盘提取码智能查询:3步告别资源获取烦恼的终极指南
  • 别再只关RST了!深入聊聊Intel快速存储技术(RAID)与Ubuntu/Linux的‘爱恨情仇’
  • Arduino旋转电位器应用:从模拟信号读取到Processing数据可视化
  • 不是所有 AI 产品都适合出海,真需求和全球化幻觉差在哪? | 嗨点小圆桌
  • 从压电传感器到示波器:手把手教你搭建电荷放大器与低通滤波器(含Multisim仿真与PCB焊接避坑指南)
  • Jetson Orin Nano + DeepStream 6.2 实战:将YOLOv5模型集成到生产级视觉流水线
  • Python爬虫实战:批量下载校园风光图
  • 10427条密码产品证书全部收集到,我发现几个数据跟认知完全对不上
  • 如何查物种的12S基因片段是否存在于NCBI公共数据库?
  • 别再傻傻用软件SPI了!实测STM32硬件SPI驱动GC9A01屏幕,速度提升10倍(附完整代码)