风格化水体的实现内容
着色:水体颜色、水体反射、水体折射、岸边泡沫、水面于天空沿边线消除、水体焦散
动画处理:水体流动、顶点动画、水体交互、水体浮力
风格化水体实现过程
(实现顺序没有严格按照着色和动画处理的分类来实现)
1.水体颜色
要制作水体颜色,要考虑的内容如下:
风格化的水体渐变颜色、水体深浅区域(进行颜色的渐变)
1.1风格化水体颜色渐变
可以到以下的网址自定义渐变色(自定义的使用cos的自定义渐变函数),并获取代码:
https://sp4ghet.github.io/grad/
因为有时无法选择其他语言的代码,也可以根据数据进行自行修改。
下面是渐变颜色函数的代码:
half4 cosine_gradient(float x, half4 phase, half4 amp, half4 freq, half4 offset)//生成渐变颜色
{
const float TAU = 2. * 3.14159265;
phase *= TAU;
x *= TAU;
return half4(offset.r + amp.r * 0.5 * cos(x * freq.r + phase.r) + 0.5,
offset.g + amp.g * 0.5 * cos(x * freq.g + phase.g) + 0.5,
offset.b + amp.b * 0.5 * cos(x * freq.b + phase.b) + 0.5,
offset.a + amp.a * 0.5 * cos(x * freq.a + phase.a) + 0.5);
}
1.2水体深浅区域
1.2.1开启深度纹理
在URP的管线配置文件中开启深度纹理
勾选Depth Texture后在Frame Debug中可以看到CopyDepth这一步,就是将相机的深度纹理缓存,同时打开Opaque Texture是因为其保存了CopyColor,是将已经绘制的不透明物体的缓存,后面会用到。
Depth Texture Mode也要设置在:渲染完不透明物体后(After Opaques),不然摄像机渲染透明水体时获取的shader深度图可能就显示不出来。
1.2.2深度纹理采样
主要实现思路:
通过获取深度纹理的采样深度(不透明物体的深度)与屏幕坐标的z值(在当前水体的裁剪空间的深度)进行相减获得差值,通过一个数(这里为1.5)减去插值来获取不同片元的一个不同的采样值,通过这个值进行渐变函数的采样,在上面的函数中,第一个参数是[0,1]的一个参数,可以获取横向(渐变参数从0到1)的一个参数变化来获取颜色(从一个颜色渐变的另一个颜色)
代码如下:
变量声明:
TEXTURE2D_X_FLOAT(_CameraDepthTexture);//采样深度图
SAMPLER(sampler_CameraDepthTexture);
在片元着色器当中:
//水体颜色渐变
//采样深度图
float depth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, i.screenPos.xy/i.screenPos.w).r;
//float depth01 = Linear01Depth(depth, _ZBufferParams);
float deptheye = LinearEyeDepth(depth, _ZBufferParams);//获取线性的[near, far]的深度值,这个深度是不透明物体的深度
float depth_D_value = saturate((deptheye - i.screenPos.z) * _depth_control);//获取场景深度与水面的距离差值,获取[0,1]之间的值(大于1取1,小于0取0,其他取中间)
//进行渐变色的定义
const half4 phases = half4(0.28, 0.50, 0.07, 0);//周期
const half4 amplitudes = half4(4.02, 0.34, 0.65, 0);//振幅
const half4 frequencies = half4(0, 0.48, 0.08, 0);//频率
const half4 offsets = half4(0, 0.16, 0, 0);//相位
half4 cos_grad = cosine_gradient(saturate(1.5 - depth_D_value), phases, amplitudes, frequencies, offsets);//进行渐变颜色计算
cos_grad = clamp(cos_grad, 0, 1);//钳制到[0,1]
Properties属性加入了控制深度的一个控制变量(可自行调整):
_depth_control("depth_control", Range(-0.2, 0.2)) = 0.01
LinearEyeDepth和Linear01Depth的定义
// Z buffer to linear 0..1 depth
float Linear01Depth(float depth, float4 zBufferParam)
{
return 1.0 / (zBufferParam.x * depth + zBufferParam.y);
}
// Z buffer to linear depth
float LinearEyeDepth(float depth, float4 zBufferParam)
{
return 1.0 / (zBufferParam.z * depth + zBufferParam.w);
}
1.3水体透明度
在此基础上为了控制水体的透明度,在Properties属性加入了控制水体透明度的控制变量:
_AlphaIntensity("AlphaIntensity", Float) = 1.0//定义一个总的透明强度控制量
在代码里,则是用这个参数来根据不同的深度调整总体的水体透明度(注意这里的一个代码思想:用此参数与深度相乘并限制到[0,1]内,这样我们可以调节参数直到相乘数值在[0,1]内,就可以看到效果了)
//下面设置一个用来控制不同深度水体的透明度的一个变量,水深则透明度低,Alpha通道数值越大,水浅则透明度高,Alpha数值越小
float Alpha_water_depth = _AlphaIntensity * depth;
Alpha_water_depth = saturate(Alpha_water_depth);//通过调整_AlphaIntensity来使Alpha_water_depth到[0,1]内读取
经过下面的混合,要对颜色的Alpha通道进行相乘操作,调整透明度:
color.a *= Alpha_water_depth;//水体透明度操作
同时为了调整透明度,需要在Properties属性加入以下混合模式的枚举:
[Enum(UnityEngine.Rendering.BlendMode)]_SrcBlend("Src Blend", Float) = 1.0//用UnityEngine.Rendering.BlendMode枚举类来作为参数
[Enum(UnityEngine.Rendering.BlendMode)]_DstBlend("Dst Blend", Float) = 0//这两个是用来分别选择混合模式的(默认为One和Zero)
然后在Pass进行混合因子的混合:
关于URP混合的操作可以参考文章:
https://zhuanlan.zhihu.com/p/137404735?utm_campaign=&utm_medium=social&utm_oi=1128416725900914688&utm_psn=1603511076779155456&utm_source=qq
控制视图摄像机的距离远近,可以看到由深度引起的一个透明度以及渐变色的一个变换(进入game模式主摄像机也可以):
2.水体流动
2.1利用程序化生成噪声图来实现水体流动
2.1.1噪声图生成
主要思路可以见下方的文章链接,这里直接用Mathf内置的PerlinNoise函数来生成噪声图,创建一个GameObject并绑定脚本,脚本里面生产纹理并传到材质球里进行采样:
脚本:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.SceneManagement;
using UnityEngine.UIElements;
using UnityEditor;
using Unity.Mathematics;
using Random = Unity.Mathematics.Random;
[ExecuteInEditMode]
public class Perlin_noise_Tex1 : MonoBehaviour
{
//图片的宽度
[SerializeField] private int pictureWidth = 1024;
//图片的高度
[SerializeField] private int pictrueHeight = 1024;
//用于柏林噪声的X采样偏移量(仿伪随机)
[SerializeField] private float xOrg = .0f;
//用于柏林噪声的Y采样偏移量(仿伪随机)
[SerializeField] private float yOrg = .0f;
//林噪声的缩放值(值越大,柏林噪声计算越密集)
[SerializeField] private float scale = 20.0f;
//最终生成的柏林噪声图
private Texture2D noiseTex;
//颜色数组
private Color[] pix;
//材质
public Material material;
private void Start()
{
//初始化噪声图
noiseTex = new Texture2D(pictureWidth, pictrueHeight);
//根据图片的宽高填充颜色数组
pix = new Color[noiseTex.width * noiseTex.height];
//将生成的柏林噪声图赋值给材质
material.SetTexture("_Perlin_noise_Tex", noiseTex);
}
private void Update()
{
//计算柏林噪声
CalcNoise();
}
//计算柏林噪声
private void CalcNoise()
{
float y = .0f;
while (y < noiseTex.height)
{
float x = .0f;
while (x < noiseTex.width)
{
//计算出X的采样值
float xCoord = xOrg + x / noiseTex.width * scale;
//计算出Y的采样值
float yCoord = yOrg + y / noiseTex.height * scale;
//用计算出的采样值计算柏林噪声
float sample = Mathf.PerlinNoise(xCoord, yCoord);
//填充颜色数组
pix[Convert.ToInt32(y * noiseTex.width + x)] = new Color(sample, sample, sample);
x++;
}
y++;
}
noiseTex.SetPixels(pix);
noiseTex.Apply();
}
}
下面是可供参考的算法代码:
下面正式进入柏林噪声的算法部分
//随机数生成函数,要针对一个二维的点产生一个二维的随机向量
Vector2 gradient(float x, float y)
{
float2 grab = new float2();
grab.x = (float)(x * 127.1 + y * 311.7);
grab.y = (float)(x * 269.5 + y * 183.3);
float sin0 = (float)(Mathf.Sin(grab.x) * 43758.5453123);
float sin1 = (float)(Mathf.Sin(grab.y) * 43758.5453123);
grab.x = (float)((sin0 - Mathf.Floor(sin0)) * 2.0 - 1.0);
grab.y = (float)((sin1 - Mathf.Floor(sin1)) * 2.0 - 1.0);
// 归一化,尽量消除正方形的方向性偏差
float len = Mathf.Sqrt(grab.x * grab.x + grab.y * grab.y);
grab.x /= len;
grab.y /= len;
return grab;
}
//计算小数部分
private Vector2 frac(Vector2 p)
{
Vector2 return_p = new Vector2();
return_p.x = p.x - Mathf.Floor(p.x);
return_p.y = p.y - Mathf.Floor(p.y);
return return_p;
}
//柏林噪声方法
private float perlin_noise(float x, float y)
{
Vector2 p = new Vector2(x, y);
Vector2 f = frac(p);
//获取四个晶格角的位置
Vector2 p00 = new Vector2(Mathf.Floor(p.x), Mathf.Floor(p.y));
Vector2 p01 = new Vector2(Mathf.Floor(p.x), Mathf.Floor(p.y) + 1);
Vector2 p10 = new Vector2(Mathf.Floor(p.x) + 1, Mathf.Floor(p.y));
Vector2 p11 = new Vector2(Mathf.Floor(p.x) + 1, Mathf.Floor(p.y) + 1);
//先获取一个晶格四个角的随机向量
Vector2 n00 = gradient(p00.x, p00.y);
Vector2 n01 = gradient(p01.x, p01.y);
Vector2 n10 = gradient(p10.x, p10.y);
Vector2 n11 = gradient(p11.x, p11.y);
//四个角分别和向量(角到取的点的向量)点积获取随机值
float v00 = Vector2.Dot(n00, p - p00);
float v01 = Vector2.Dot(n01, p - p01);
float v10 = Vector2.Dot(n10, p - p10);
float v11 = Vector2.Dot(n11, p - p11);
//下面设置缓和曲线的参数(用来插值),先横向后纵向
float t0 = (float)(6.0 * Mathf.Pow(f.x, 5) - 15.0 * Mathf.Pow(f.x, 4) +
10.0 * Mathf.Pow(f.x, 3)); //缓和函数为y = 6x^5 - 15x^4 + 10x^3 区间 [0, 1]
float t1 = (float)(6.0 * Mathf.Pow(f.y, 5) - 15.0 * Mathf.Pow(f.y, 4) +
10.0 * Mathf.Pow(f.y, 3)); //缓和函数为y = 6x^5 - 15x^4 + 10x^3 区间 [0, 1]
float perlin_noise_value = (float)(Mathf.Lerp(Mathf.Lerp(v00, v10, t0), Mathf.Lerp(v01, v11, t0), t1));
return perlin_noise_value;
}
Shader里面采样:
TEXTURE2D(_Perlin_noise_Tex);//采样柏林噪声贴图
SamplerState sampler_Perlin_noise_Tex;
片元着色器里:
//采样噪声贴图
half4 perlin_noise_color = _Perlin_noise_Tex.Sample(sampler_Perlin_noise_Tex, i.uv);
脚本调节面板:
效果图
可参考文章:
生成噪声图:
https://zhuanlan.zhihu.com/p/52054806?utm_campaign=&utm_medium=social&utm_oi=1128416725900914688&utm_psn=1603512422333042689&utm_source=qq
https://zhuanlan.zhihu.com/p/240763739?utm_campaign=&utm_medium=social&utm_oi=1128416725900914688&utm_psn=1603512802643251200&utm_source=qq
创建程序化纹理贴图:https://www.cnblogs.com/alps/p/7791745.html
UnityShader学习——程序纹理:https://blog.csdn.net/qq_36622009/article/details/105594236
图形学中常见噪声生成算法综述:https://www.jianshu.com/p/9cfb678fbd95
unity中噪声函数和应用:https://blog.csdn.net/js0907/article/details/110414282
(!!!)使用Unity生成各类型噪声图的分享:https://zhuanlan.zhihu.com/p/463369923
(!!!)WebGL进阶——走进图形噪声:https://zhuanlan.zhihu.com/p/68507311
官方文档:https://docs.unity.cn/cn/2019.4/ScriptReference/Random.Range.html
随机数产生:
https://blog.csdn.net/weixin_38211198/article/details/90680412
Unity-get/set方法:
https://blog.csdn.net/Mr_Sun88/article/details/84202382
柏林噪声的算法原理:
(!!!)PerlinNoise-C#柏林噪声的探讨与实现:
https://blog.csdn.net/kuangben2000/article/details/102511295?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22102511295%22%2C%22source%22%3A%22Phantom1516%22%7D
[Nature of Code] 柏林噪声:
https://zhuanlan.zhihu.com/p/206271895?utm_campaign=&utm_medium=social&utm_oi=1128416725900914688&utm_psn=1605356528265048064&utm_source=qq
2.1.2采样噪声图
上面只是展示了噪声图的生成获取,接着要讲一下怎么进行采样才能实现流动的效果。
我们会用到控制时间的float4变量_Time,具体的内容如下图所示:
所以shader中我们一般用_Time.y来表示时间,这里 t 其实就是Time.timeSinceLevelLoad,每次切完场景会重新变化计数,我们会用到变量_Time.y来作为时间进行噪声图的采样。
//生成海浪,这个函数主要是进行法线的扰动
float3 swell_normal(float4 position_WS, half4 perlin_noise_color)//传进来的参数是经过插值之后的一个世界坐标、噪声颜色值
{
//噪声颜色值,将作为该片元插值顶点世界坐标对应的高度值,用不同片元的高度值来产生海浪
float height = perlin_noise_color;//获取高度的世界空间向量
float3 normal = cross(
float3(0, ddy(height), 1.0),
float3(1, ddx(height), 0)
);//获取扰动后的一个法线
return normalize(normal);
}
片元着色器
half4 frag (v2f i) : SV_Target
{
//采样噪声贴图
float2 uv = float2(i.uv.x, i.uv.y + _Time.y);
half4 perlin_noise_color = _Perlin_noise_Tex.Sample(sampler_Perlin_noise_Tex, uv);
i.normal_WS = swell_normal(i.position_WS, perlin_noise_color);//获取一个新的法线
...
}
下面是随着时间的变化噪声图采样的结果的截图(这里是朝着v方向流动的一个效果,返回值是噪声的颜色值),这里有了一些水面波纹的效果
近处因为像素间的颜色相差比较小,所以法线扰动不明显,远处的话扰动就比较明显了。
但感觉效果不是很好,下面基于此方法,再使用FlowMap进行优化。
2.2FlowMap方法
2.2.1FlowMap原理实现
下面要讲的方法是使用FlowMap叠加位移贴图实现的流动水面。
half4 frag (v2f i) : SV_Target
{
//采样FlowMap
float4 FlowMap_Dir = _FlowMap.Sample(sampler_FlowMap, i.uv) * 2 - 1;//获取FlowMap采样的向量方向,即此处的流动方向
//FlowMap的采样设置
//要让FlowMap流动,需要和时间进行相乘,但因为随着时间叠加,扭曲会更加严重,所以先用frac函数将增量都限制在[0,1]小数内,形成一个三角波的形式
//但是一个三角波会有很明显的断层跳变归0的现象,我们要有两个波来加权叠加,使纹理流动一个周期重新开始的不自然情况被另外一层采样覆盖
float F_phase0 = frac(_Time.y * 0.1 * _FlowMapSpeed);//第一个三角波,乘上0.1是为了让时间初始以0.1的量增加
float F_phase1 = frac(_Time.y * 0.1 * _FlowMapSpeed + 0.5);//第二个三角波,和第一个相比多了0.5个相位,注意是提前!!!
//先定义一下当前平铺贴图的插值后uv坐标
float2 F_tiling_uv = i.uv * _FlowMap_ST.xy + _FlowMap_ST.zw;
//下面获取每一个时间单位两个波获取的颜色,采样的位置是当前片元获取的方向的xy(指向下一个时间单位采样的方向)分别乘上两个波得出的一个增量,得到下一个时间单位的采样点
//在同一个时间点采样了两个位置的颜色值,要进行加权得到最终的颜色值,这里体现了FlowMap的作用,提供了偏移的计算,但还是在原图(水面贴图)上进行操作
half4 F_color0 = _Water_surface_ripple_Tex.Sample(sampler_Water_surface_ripple_Tex, F_tiling_uv + F_phase0 * FlowMap_Dir.xy + perlin_noise_color.xy * _Noise_intensity);
half4 F_color1 = _Water_surface_ripple_Tex.Sample(sampler_Water_surface_ripple_Tex, F_tiling_uv + F_phase1 * FlowMap_Dir.xy + perlin_noise_color.yz * _Noise_intensity);
//下面通过权重来对两个采样的颜色进行平滑,因为在F0_phase0达到最大偏移值时,如果权重很大就会有明显的跳变,所以在此时F0_phase0权重为0
//而在时间为半个周期的时候,F_phase1刚好跳变,此时F_phase1权重应该最小,F_phase0权重应该最大
//所以给出下面的权重函数(横坐标为F_phase0,取值[0,1],纵坐标为F_phase1的权重,F_phase0为0.5的时候,F_phase1刚好是0,权重为最小):
float F_lerp = abs(2 * (0.5 - F_phase0));//获取的是F_phase1的权重
half4 F_color = lerp(F_color0, F_color1, F_lerp);//获取最终偏移的颜色,F_color0 * (1 - F_lerp) + F_color1 * F_lerp
...
}
记得FlowMap只是提供扰乱的方法,具体实现要在水面的贴图,上面的代码中暂时用FlowMap对一张水面法线贴图进行扰动。
可参考的原理分析的资料:
【技术美术百人计划】图形 2.8 flowmap的实现——流动效果实现:
https://www.bilibili.com/video/BV1Zq4y157c9/?buvid=XYDC7F2D1F323E880C3B33DDA5170A8AAE9EC&is_story_h5=false&mid=ha3waLVooMnX1T5sP6Mutg%3D%3D&p=2&plat_id=116&share_from=ugc&share_medium=android&share_plat=android&share_session_id=e77f3183-dc90-4ff2-8205-d7593aadd4c5&share_source=QQ&share_tag=s_i×tamp=1675676250&unique_k=lJ53Cl5&up_id=7398208&vd_source=1889bcd37d239b47cf8a5296ad456124
图形学基础——Flowmap的实现:https://blog.csdn.net/whitebreeze/article/details/117970673
FlowMap的简单应用:https://zhuanlan.zhihu.com/p/511518080?utm_campaign=&utm_medium=social&utm_oi=1128416725900914688&utm_psn=1606011723374309376&utm_source=qq
2.2.2FlowMap扰动+噪声图扰动
单单用FlowMap个人感觉还差了点意思,所以用第一个方案生成的噪声贴图来进行采样,具体是通过采样的噪声颜色的rg和gb两个通道(分别作为float2值)分别在FlowMap两个波采样时uv进一步进行扰动,代码如下:
Properties属性:
_Noise_intensity("_Noise_intensity", Range(0, 1.5)) = 1.0//设置在FlowMap基础上加入噪声扰动的偏移强度
片元着色器:
//采样FlowMap
float4 FlowMap_Dir = _FlowMap.Sample(sampler_FlowMap, i.uv) * 2 - 1;//获取FlowMap采样的向量方向,即此处的流动方向
//FlowMap的采样设置
//要让FlowMap流动,需要和时间进行相乘,但因为随着时间叠加,扭曲会更加严重,所以先用frac函数将增量都限制在[0,1]小数内,形成一个三角波的形式
//但是一个三角波会有很明显的断层跳变归0的现象,我们要有两个波来加权叠加,使纹理流动一个周期重新开始的不自然情况被另外一层采样覆盖
float F_phase0 = frac(_Time.y * 0.1 * _FlowMapSpeed);//第一个三角波,乘上0.1是为了让时间初始以0.1的量增加
float F_phase1 = frac(_Time.y * 0.1 * _FlowMapSpeed + 0.5);//第二个三角波,和第一个相比多了0.5个相位,注意是提前!!!
//先定义一下当前平铺贴图的插值后uv坐标
float2 F_tiling_uv = i.uv * _FlowMap_ST.xy + _FlowMap_ST.zw;
//在完成FlowMap采样设置的基础上再用噪声进一步扰动
//先定义噪声贴图采样的坐标
float2 _Perlin_noise_Tex_uv = i.uv;
//进行噪声贴图的采样,采样的值取出rg通道和gb通道作为两个float2强度值在分别在FlowMap的两次采样uv基础上进行进一步uv偏移
float4 perlin_noise_color = _Perlin_noise_Tex.Sample(sampler_Perlin_noise_Tex, _Perlin_noise_Tex_uv);
//下面获取每一个时间单位两个波获取的颜色,采样的位置是当前片元获取的方向的xy(指向下一个时间单位采样的方向)分别乘上两个波得出的一个增量,得到下一个时间单位的采样点
//在同一个时间点采样了两个位置的颜色值,要进行加权得到最终的颜色值,这里体现了FlowMap的作用,提供了偏移的计算,但还是在原图(水面贴图)上进行操作
half4 F_color0 = _Water_surface_ripple_Tex.Sample(sampler_Water_surface_ripple_Tex, F_tiling_uv + F_phase0 * FlowMap_Dir.xy + perlin_noise_color.xy * _Noise_intensity);
half4 F_color1 = _Water_surface_ripple_Tex.Sample(sampler_Water_surface_ripple_Tex, F_tiling_uv + F_phase1 * FlowMap_Dir.xy + perlin_noise_color.yz * _Noise_intensity);
//下面通过权重来对两个采样的颜色进行平滑,因为在F0_phase0达到最大偏移值时,如果权重很大就会有明显的跳变,所以在此时F0_phase0权重为0
//而在时间为半个周期的时候,F_phase1刚好跳变,此时F_phase1权重应该最小,F_phase0权重应该最大
//所以给出下面的权重函数(横坐标为F_phase0,取值[0,1],纵坐标为F_phase1的权重,F_phase0为0.5的时候,F_phase1刚好是0,权重为最小):
float F_lerp = abs(2 * (0.5 - F_phase0));//获取的是F_phase1的权重
half4 F_color = lerp(F_color0, F_color1, F_lerp);//获取最终偏移的颜色,F_color0 * (1 - F_lerp) + F_color1 * F_lerp
...
}
进一步扰动后我们可以调节噪声图扰动强度,以及在脚本面板中调整噪声图的生成各属性来获得更好的一个自定义流动效果(代码返回的是贴图采样的流动结果):
2.2.3实现水体表面的流动
要将扰动之后的贴图应用到水体,主要是进行法线的扰动,所以通过上面介绍的FlowMap扰动方法,就可以对bump贴图进行采样操作。
片元着色器:
//水体流动shader
//采样FlowMap
float4 FlowMap_Dir = _FlowMap.Sample(sampler_FlowMap, i.uv) * 2 - 1;//获取FlowMap采样的向量方向,即此处的流动方向
//FlowMap的采样设置
//要让FlowMap流动,需要和时间进行相乘,但因为随着时间叠加,扭曲会更加严重,所以先用frac函数将增量都限制在[0,1]小数内,形成一个三角波的形式
//但是一个三角波会有很明显的断层跳变归0的现象,我们要有两个波来加权叠加,使纹理流动一个周期重新开始的不自然情况被另外一层采样覆盖
float F_phase0 = frac(_Time.y * 0.1 * _FlowMapSpeed);//第一个三角波,乘上0.1是为了让时间初始以0.1的量增加
float F_phase1 = frac(_Time.y * 0.1 * _FlowMapSpeed + 0.5);//第二个三角波,和第一个相比多了0.5个相位,注意是提前!!!
//先定义一下当前平铺贴图的插值后uv坐标
float2 F_tiling_uv = i.uv * _FlowMap_ST.xy + _FlowMap_ST.zw;
//在完成FlowMap采样设置的基础上再用噪声进一步扰动
//先定义噪声贴图采样的坐标
float2 _Perlin_noise_Tex_uv = i.uv;
//进行噪声贴图的采样,采样的值取出rg通道和gb通道作为两个float2强度值在分别在FlowMap的两次采样uv基础上进行进一步uv偏移
float4 perlin_noise_color = _Perlin_noise_Tex.Sample(sampler_Perlin_noise_Tex, _Perlin_noise_Tex_uv);
//下面获取每一个时间单位两个波获取的颜色,采样的位置是当前片元获取的方向的xy(指向下一个时间单位采样的方向)分别乘上两个波得出的一个增量,得到下一个时间单位的采样点
//在同一个时间点采样了两个位置的颜色值,要进行加权得到最终的颜色值,这里体现了FlowMap的作用,提供了偏移的计算,但还是在原图(水面贴图)上进行操作
//采样两张水面波纹法线贴图,进行法线扰动
float3 Water_surface_ripple_normal1 = UnpackNormal(_Water_surface_ripple_normal_Tex1.Sample(sampler_Water_surface_ripple_normal_Tex1, F_tiling_uv + F_phase0 * FlowMap_Dir.xy + perlin_noise_color.xy * _Noise_intensity));
float3 Water_surface_ripple_normal2 = UnpackNormal(_Water_surface_ripple_normal_Tex1.Sample(sampler_Water_surface_ripple_normal_Tex1, F_tiling_uv + F_phase1 * FlowMap_Dir.xy + perlin_noise_color.xy * _Noise_intensity));
//下面通过权重来对两个采样的颜色进行平滑,因为在F_phase0达到最大偏移值时,如果权重很大就会有明显的跳变,所以在此时F_phase0权重为0
//而在时间为半个周期的时候,F_phase1刚好跳变,此时F_phase1权重应该最小,F_phase0权重应该最大
//所以给出下面的权重函数(横坐标为F_phase0,取值[0,1],纵坐标为F_phase1的权重,F_phase0为0.5的时候,F_phase1刚好是0,权重为最小):
float F_lerp = abs(2 * (0.5 - F_phase0));//获取的是F_phase1的权重
float3 Water_surface_ripple_normal = normalize(lerp(Water_surface_ripple_normal1, Water_surface_ripple_normal2, F_lerp));
Water_surface_ripple_normal = TransformTangentToWorldDir(Water_surface_ripple_normal, i.TtoW);
float3 normal_WS = normalize(i.normal_WS + Water_surface_ripple_normal);
float4 SHADOW_COORDS = TransformWorldToShadowCoord(i.position_WS);//获取将世界空间的顶点坐标转换到光源空间获取的阴影坐标,这里在片元着色器里面进行,利用了插值之后的结果
Light main_light = GetMainLight(SHADOW_COORDS);
float3 light_direction_WS = normalize(TransformObjectToWorld(main_light.direction));//获取世界空间的光照单位矢量
float3 view_direct_WS = normalize(i.view_direct_WS);
float3 reflec_direct_WS = normalize(i.reflec_direct_WS);
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//获取环境光强度
half3 diffuse = main_light.color.rgb * _Diffuse.rgb * saturate(dot(normal_WS, light_direction_WS));//获取漫反射强度,light.color.rgb是光照强度
half3 reflection = texCUBE(_Reflection_CubeMap,reflec_direct_WS).rgb * _ReflectColor.rgb;//获取反射的天空盒子的颜色
half3 half_dir = normalize(light_direction_WS + view_direct_WS);//获取半程向量
half3 specular = main_light.color.rgb * _Specular.rgb * pow(saturate(dot(half_dir,normal_WS)), _Gloss);//获取高光
reflection = lerp(diffuse, reflection, _ReflectAmount);//通过平滑函数获得最佳的反射
float fresnel = pow(1 - dot(normal_WS, view_direct_WS), _Fresnel);//菲涅尔
half4 reflection_map_color = _Reflection_camera_RT.Sample(sampler_Reflection_camera_RT, i.screenPos.xy / i.position_CS.w);//采样反射纹理贴图
half4 color = half4(ambient + fresnel * reflection + specular, 1.0) * main_light.shadowAttenuation;//获取光照和天空盒的采样颜色
//下面设置一个用来控制不同深度水体的透明度的一个变量,水深则透明度低,Alpha通道数值越大,水浅则透明度高,Alpha数值越小
float Alpha_water_depth = _AlphaIntensity * depth;
Alpha_water_depth = saturate(Alpha_water_depth);//通过调整_AlphaIntensity来使Alpha_water_depth到[0,1]内读取
color = lerp(cos_grad, color, _water_color_Amount);
color.a *= Alpha_water_depth;//水体透明度操作
color = lerp(reflection_map_color, color, _reflection_map_color_Amount);
return color;
在此基础上,我决定再采样一张水面波纹纹理来进行叠加扰动:
顶点着色器:
//进行uv扰动
o.detail_uv.xy = o.position_WS.xz * 0.4 - _Time.y * 0.1;
o.detail_uv.zw = o.position_WS.xz * 0.1 + _Time.y * 0.05;
片元着色器:
//水体流动shader
//采样FlowMap
float4 FlowMap_Dir = _FlowMap.Sample(sampler_FlowMap, i.uv) * 2 - 1;//获取FlowMap采样的向量方向,即此处的流动方向
//FlowMap的采样设置
//要让FlowMap流动,需要和时间进行相乘,但因为随着时间叠加,扭曲会更加严重,所以先用frac函数将增量都限制在[0,1]小数内,形成一个三角波的形式
//但是一个三角波会有很明显的断层跳变归0的现象,我们要有两个波来加权叠加,使纹理流动一个周期重新开始的不自然情况被另外一层采样覆盖
float F_phase0 = frac(_Time.y * 0.1 * _FlowMapSpeed);//第一个三角波,乘上0.1是为了让时间初始以0.1的量增加
float F_phase1 = frac(_Time.y * 0.1 * _FlowMapSpeed + 0.5);//第二个三角波,和第一个相比多了0.5个相位,注意是提前!!!
//先定义一下当前平铺贴图的插值后uv坐标
float2 F_tiling_uv = i.uv * _FlowMap_ST.xy + _FlowMap_ST.zw;
//在完成FlowMap采样设置的基础上再用噪声进一步扰动
//先定义噪声贴图采样的坐标
float2 _Perlin_noise_Tex_uv = i.uv;
//进行噪声贴图的采样,采样的值取出rg通道和gb通道作为两个float2强度值在分别在FlowMap的两次采样uv基础上进行进一步uv偏移
float4 perlin_noise_color = _Perlin_noise_Tex.Sample(sampler_Perlin_noise_Tex, _Perlin_noise_Tex_uv);
//下面获取每一个时间单位两个波获取的颜色,采样的位置是当前片元获取的方向的xy(指向下一个时间单位采样的方向)分别乘上两个波得出的一个增量,得到下一个时间单位的采样点
//在同一个时间点采样了两个位置的颜色值,要进行加权得到最终的颜色值,这里体现了FlowMap的作用,提供了偏移的计算,但还是在原图(水面贴图)上进行操作
//采样两张水面波纹法线贴图,进行法线扰动
float3 Water_surface_ripple_normal1 = UnpackNormal(_Water_surface_ripple_normal_Tex1.Sample(sampler_Water_surface_ripple_normal_Tex1, F_tiling_uv + F_phase0 * FlowMap_Dir.xy + perlin_noise_color.xy * _Noise_intensity));
float3 Water_surface_ripple_normal2 = UnpackNormal(_Water_surface_ripple_normal_Tex1.Sample(sampler_Water_surface_ripple_normal_Tex1, F_tiling_uv + F_phase1 * FlowMap_Dir.xy + perlin_noise_color.xy * _Noise_intensity));
//下面通过权重来对两个采样的颜色进行平滑,因为在F_phase0达到最大偏移值时,如果权重很大就会有明显的跳变,所以在此时F_phase0权重为0
//而在时间为半个周期的时候,F_phase1刚好跳变,此时F_phase1权重应该最小,F_phase0权重应该最大
//所以给出下面的权重函数(横坐标为F_phase0,取值[0,1],纵坐标为F_phase1的权重,F_phase0为0.5的时候,F_phase1刚好是0,权重为最小):
float F_lerp = abs(2 * (0.5 - F_phase0));//获取的是F_phase1的权重
float3 Water_surface_ripple_normal = normalize(lerp(Water_surface_ripple_normal1, Water_surface_ripple_normal2, F_lerp));
Water_surface_ripple_normal = TransformTangentToWorldDir(Water_surface_ripple_normal, i.TtoW);
float3 normal_WS = normalize(i.normal_WS + Water_surface_ripple_normal);
//再采样一张水面纹理贴图
float3 bump_normal1 = UnpackNormal(_Water_surface_ripple_normal_Tex2.Sample(sampler_Water_surface_ripple_normal_Tex2, i.detail_uv.xy));
float3 bump_normal2 = UnpackNormal(_Water_surface_ripple_normal_Tex2.Sample(sampler_Water_surface_ripple_normal_Tex2, i.detail_uv.zw));
float3 bump_normal = normalize(0.5 * bump_normal1 + bump_normal2);//获取第二张图扰动后的法线
normal_WS = normalize(normal_WS + float3(bump_normal.x, 0, bump_normal.y));
3.顶点动画
3.1Sin叠加波浪效果
正弦波是最基本用于模拟海浪形状的波形,用于改变顶点位置,我们要计算它在各个方向上的顶点位移和法线。
实现之前,先定义波函数相关参数:
3.1.1顶点位移
x、z代表水平方向,不发生位移,y轴代表垂直方向,则顶点的位移公式如下(简谐波运动公式):
y(x, z, t) = A * sin[w * ((dx, dz) * (x, z) + t * s)]
3.1.2法线计算
首先要知道法线如何获取,法线可以由切线和副切线叉乘得到,水体是三维空间中的曲面,切线沿x轴,副切线沿z轴,所以可以分别看成是曲面对x、z方向的偏导数。
曲面方程
在三维空间内的曲面方程:
P(x, z, t) = (x, y(x, z, t), z)
切线向量
推导如下:
副切线向量
推导如下:
法线向量
将副切线向量和切线向量进行叉乘:
到这里就成功获取顶点动画中的顶点的新法线。
3.1.3代码实现
先定义一个波浪的结构体:
struct WaveStruct
{
float3 normal;//定义获取的顶点动画新的顶点法线
float3 position;//定义获取的顶点动画新的顶点位置
};
实现Sin曲线的主体函数:
//(顶点动画)顶点位移公式Sin曲线生成
WaveStruct SinWave(float4 pos, half angle)//需要参与计算的参数为顶点模型坐标,振幅,波长,风吹动的角度
{
WaveStruct waveout;
half w = 2 * 3.14159 / _Wavelength;//获取频率
angle = radians(angle);//将度数转化为弧度
float2 direction = float2(cos(angle), sin(angle));//通过风的角度(二维平面)来得到顶点的二维平面uv两个方向的移动距离,由此来定位点的移动方向(二维)
half dir = dot(direction, pos.xz);//使用模型空间下的顶点坐标
half calc = w * dir + _WaveSpeed * _Time.y;//计算出公式的sin()括号里面的计算结果
half Sin_calc = sin(calc);
half Cos_calc = cos(calc);//用于后面计算偏导
waveout.position = 0;//初始化
waveout.position.y = _Amplitude * Sin_calc * _WaveCountMulti;//得到顶点偏移的y轴数值
//下面进行法线计算
waveout.normal = normalize(float3(
-w * direction.x * _Amplitude * Cos_calc,
1,
-w * direction.y * _Amplitude * Cos_calc));
return waveout;
}
顶点着色器:
half windangle = 30;//设置风的吹动角度
WaveStruct wave1 = SinWave(v.vertex, windangle);
v.vertex = v.vertex + float4(wave1.position.xyz, 0);//获取新的顶点位置
o.normal_WS = wave1.normal.xyz;//获取新法线
换了一个场景实现:
3.2Gerstner波
Gerstner波描绘的是在水面上下波动的同时,xz也会偏移。在水面上的一点接近峰值时,会向着波峰移动,波峰消散时会向着相反的方向移动,实现波峰更加尖锐,波谷更加平坦,具体如下图所示:
3.2.1顶点位移
曲面方程为:
注意:Qi是坡度,Qi为0的时候是正弦波,Qi为1/(wi * Ai)时得到一个尖峰波形
3.2.2法线计算
计算过程和Sin波类似,这里给出计算之后的切线、副切线、法线的向量:
对比之前的浪(这里夸张处理了一下):
Sin波:
Gerstner波:
很明显,波峰波谷的效果要好很多
下面是调整好的图:
叠加了多个波后:
多个Gerstner Wave叠加的实现代码:
属性Properties:
//波浪的控制速度
_WaveSpeed1("WaveSpeed1", Float) = 1.0
//波浪相位
_Amplitude1("Amplitude1", Float) = 1.0
//波浪波长
_Wavelength1("Wavelength1", Float) = 1.0
//风向(二维平面)
_Windangle1("Windangle1", Float) = 30
//坡度,为0的时候是正弦波,为1/(wi * Ai)时得到一个尖峰波形
_Slope1("Slope1", Float) = 1.0
//波浪的控制速度
_WaveSpeed2("WaveSpeed2", Float) = 1.0
//波浪相位
_Amplitude2("Amplitude2", Float) = 1.0
//波浪波长
_Wavelength2("Wavelength2", Float) = 1.0
//风向(二维平面)
_Windangle2("Windangle2", Float) = 30
//坡度,为0的时候是正弦波,为1/(wi * Ai)时得到一个尖峰波形
_Slope2("Slope2", Float) = 1.0
//波浪的控制速度
_WaveSpeed3("WaveSpeed3", Float) = 1.0
//波浪相位
_Amplitude3("Amplitude3", Float) = 1.0
//波浪波长
_Wavelength3("Wavelength3", Float) = 1.0
//风向(二维平面)
_Windangle3("Windangle3", Float) = 30
//坡度,为0的时候是正弦波,为1/(wi * Ai)时得到一个尖峰波形
_Slope3("Slope3", Float) = 1.0
//波浪的控制速度
_WaveSpeed4("WaveSpeed4", Float) = 1.0
//波浪相位
_Amplitude4("Amplitude4", Float) = 1.0
//波浪波长
_Wavelength4("Wavelength4", Float) = 1.0
//风向(二维平面)
_Windangle4("Windangle4", Float) = 30
//坡度,为0的时候是正弦波,为1/(wi * Ai)时得到一个尖峰波形
_Slope4("Slope4", Float) = 1.0
生成GerstnerWave波:
//生成GerstnerWave波
WaveStruct GerstnerWave(float4 pos, float amplitude, float waveSpeed, float wavelength, float windangle, float slope)
{
WaveStruct waveout;
half w = 2 * 3.14159 / wavelength;//获取频率
float angle = radians(windangle);//将度数转化为弧度
float2 direction = float2(cos(angle), sin(angle));//通过风的角度(二维平面)来得到顶点的二维平面uv两个方向的移动距离,由此来定位点的移动方向(二维)
half dir = dot(direction, pos.xz);//使用模型空间下的顶点坐标
half calc = w * dir - waveSpeed * _Time.y;//计算出公式的sin()括号里面的计算结果
half Sin_calc = sin(calc);
half Cos_calc = cos(calc);//用于后面计算偏导
//下面计算顶点偏移
waveout.position = 0;//初始化
waveout.position.x = slope * amplitude * direction.x * Cos_calc;
waveout.position.y = amplitude * Sin_calc;//得到顶点偏移的y轴数值
waveout.position.z = slope * amplitude * direction.y * Cos_calc;
//下面进行法线计算
waveout.normal = normalize(float3(
-w * direction.x * amplitude * Cos_calc,
1 - slope * amplitude * w * Sin_calc,
-w * direction.y * amplitude * Cos_calc));
return waveout;
}
注意:
half calc = w * dir - waveSpeed * _Time.y;//计算出公式的sin()括号里面的计算结果
这里 + or - waveSpeed * _Time.y只是运动的方向不同而已
顶点着色器:
//计算四个波
WaveStruct wave1 = GerstnerWave(v.vertex, _Amplitude1, _WaveSpeed1, _Wavelength1, _Windangle1, _Slope1);
WaveStruct wave2 = GerstnerWave(v.vertex, _Amplitude2, _WaveSpeed2, _Wavelength2, _Windangle2, _Slope2);
WaveStruct wave3 = GerstnerWave(v.vertex, _Amplitude3, _WaveSpeed3, _Wavelength3, _Windangle3, _Slope3);
WaveStruct wave4 = GerstnerWave(v.vertex, _Amplitude4, _WaveSpeed4, _Wavelength4, _Windangle4, _Slope4);
v.vertex = v.vertex + float4(wave1.position.xyz, 0) + float4(wave2.position.xyz, 0) + float4(wave3.position.xyz, 0) + float4(wave4.position.xyz, 0);//获取新的顶点位置
o.normal_WS = normalize(wave1.normal.xyz + wave2.normal.xyz + wave3.normal.xyz + wave4.normal.xyz);//获取新法线
4.岸边泡沫
这里要实现的基本效果是岸边泡沫。
4.1边缘产生
这里我们用水体与深度图的差值来实现,但有一个问题,如果只是采用普通的深度图,那随着相机的移动相对深度会发生改变,浮沫的范围就变得不可控,那如何产生不受相机移动的很稳定可控的浮沫呢,我这里采用的是深度图世界坐标重建的方法,通过得出深度图点的世界坐标,和水体世界坐标的y轴做差值,就可以得到很稳定的一个可控浮沫,具体的实现代码如下:
顶点着色器:
//获取当前点在摄像机空间的视锥向量
float4 ndcPos = (o.screenPos / o.screenPos.w) * 2 - 1;//将屏幕空间的点转换到NDC空间
float far = _ProjectionParams.z;//远平面的设定值
float3 clipVec = float3(ndcPos.x, ndcPos.y, -1.0) * far;//获取裁剪空间的坐标向量
o.viewVec = mul(unity_CameraInvProjection, clipVec.xyzz).xyz;//获取观察空间的坐标向量
注意:
float3 clipVec = float3(ndcPos.x, ndcPos.y, -1.0) * far;//获取裁剪空间的坐标向量
这里的-1.0是因为裁剪空间到相机空间的z轴反转了,还有,这里构建的是一个向量o.viewVec,起点为相机原点,终点是在远平面上的一点
下面产生边缘的计算图:
片元着色器:
//边缘产生
float3 viewPos = i.viewVec * depth01;//乘以线性深度
float4 position_WS = mul(unity_CameraToWorld, float4(viewPos, 1));//将观察空间坐标转化到世界空间
float depth_poam_D_value = position_WS.y - i.position_WS.y;
float depthEdge = saturate(_DeptheyeEdge * depth_poam_D_value);//得到线性的真实深度差值,并将深度差值调整到合适的范围内(用一个系数_DeptheyeEdge进行调整后取[0,1]内的数值)
返回half4(depthEdge,depthEdge,depthEdge,1),效果如下:
调整系数后就可以产生非常稳定的边缘。
参考文章:
【UnityShader】从深度图重建世界坐标(1)
https://zhuanlan.zhihu.com/p/547213235?utm_campaign=&utm_medium=social&utm_oi=1128416725900914688&utm_psn=1607502583488352256&utm_source=qq
4.2岸边泡沫实现
我们已经获得了:可以自定义调节的边缘范围,深度值过渡从0到1,我们就可以利用这个深度值和采样的泡沫贴图进行相乘并用一个系数来调整。
片元着色器,在上面代码的基础上:
//岸边泡沫
float2 foam_uv = i.uv * _FoamMap_ST.xy + _FoamMap_ST.zw;
half4 foam_color = _FoamMap.Sample(sampler_FoamMap, foam_uv);//采样泡沫贴图
float3 viewPos = i.viewVec * depth01;//乘以线性深度
float4 position_WS = mul(unity_CameraToWorld, float4(viewPos, 1));
float depth_poam_D_value = position_WS.y - i.position_WS.y;
float depthEdge = saturate(_DeptheyeEdge * depth_poam_D_value);//得到线性的真实深度差值,并将深度差值调整到合适的范围内(用一个系数_DeptheyeEdge进行调整后取[0,1]内的数值)
float foam_value = saturate(1 - foam_color * depthEdge * _Foam_edge_control);//获取浮沫,因为越靠近岸深度值越小,泡沫颜色却要最白,所以要1减去和深度相乘的数值
加入smoostep调整去掉一些杂波:
foam_value = smoothstep(0.4, 1, foam_value);//去掉一些杂波
最终效果:
4.3岸边泡沫风格化调整优化
首先是修改Tiling和Offset使白沫成条状,同时加入sin函数进行取值,原因是sin函数在[0, 2*3.14]的取值范围内有正负的两段取值,可以来得到白沫到与岸的接触点处白沫消失。
//岸边泡沫
float2 foam_uv = i.uv * _FoamMap_ST.xy + _FoamMap_ST.zw;
half4 foam_color = _FoamMap.Sample(sampler_FoamMap, foam_uv);//采样泡沫贴图
float3 viewPos = i.viewVec * depth01;//乘以线性深度
float4 r_under_position_WS = mul(unity_CameraToWorld, float4(viewPos, 1));
float depth_D_WS_value = r_under_position_WS.y - i.position_WS.y;
float depthEdge = saturate(_DeptheyeEdge * depth_D_WS_value);//得到线性的真实深度差值,并将深度差值调整到合适的范围内(用一个系数_DeptheyeEdge进行调整后取[0,1]内的数值)
float foam_value = saturate(sin((1 - foam_color * depthEdge * _Foam_edge_control) * 6.28));//获取浮沫,因为越靠近岸深度值越小,泡沫颜色却要最白,所以要1减去和深度相乘的数值
foam_value = smoothstep(0.35, 1, foam_value);//去掉一些杂波
但缺点很明显,并没有实现白沫消散的过程,而是直接就分成有白沫和没有白沫的两段。
如果代码修改如下:
float foam_value = saturate(sin((1 - foam_color * depthEdge * _Foam_edge_control) * 6.28 - _Time.y));
调整过后会有接续的白沫带产生,但是外海区域(0)会由于时间叠加而周期性变白,原因是:在之前计算的depthEdge是[0,1]的范围,而外海区域是0,所以1 - foam_color * depthEdge * _Foam_edge_control
始终是1,和_Time.y(决定流动方向而已)进行相减后,横坐标变换,原来是0的sin取值就不再是0,而是在[0,1]之间波动,就会有周期性的一个变白的过程,所以思考之后的解决方法如下:
设置一个遮罩,让第二条波纹产生时不是整个外海都变白色,而是只有第二条波纹的边缘处是白色,而且要从无到有的缓慢产生,所以考虑了采样的画图如下:
当采样函数sin的值[0,1]部分逐渐采样的白沫带,我们只需要在整块外海变白的时候颜色取值为0,产生白沫带后再恢复颜色就可以,所以只要乘上一个如图从0变化到1的系数即可,而之前我们已经是得到这样一个值的:1 - foam_color * depthEdge * _Foam_edge_control
,所以乘上这个数就可以。
float foam_value = saturate(sin((1 - foam_color * depthEdge * _Foam_edge_control) * 6.28 - _Time.y)) * 2 * (1 - foam_color * depthEdge * _Foam_edge_control);//获取浮沫,因为越靠近岸深度值越小,泡沫颜色却要最白,所以要1减去和深度相乘的数值
但是现在两条白沫带的频率比较低,会出现没有白沫带的时候,我们可以调节系数来得到我们的效果
//岸边泡沫
float2 foam_uv = i.uv * _FoamMap_ST.xy + _FoamMap_ST.zw;
half4 foam_color = _FoamMap.Sample(sampler_FoamMap, foam_uv);//采样泡沫贴图
float3 viewPos = i.viewVec * depth01;//乘以线性深度
float4 position_WS = mul(unity_CameraToWorld, float4(viewPos, 1));
float depth_poam_D_value = position_WS.y - i.position_WS.y;
float depthEdge = saturate(_DeptheyeEdge * depth_poam_D_value);//得到线性的真实深度差值,并将深度差值调整到合适的范围内(用一个系数_DeptheyeEdge进行调整后取[0,1]内的数值)
//float foam_value = saturate(1 - foam_color * depthEdge * _Foam_edge_control);
float foam_value = saturate(sin((1 - foam_color * depthEdge * _Foam_edge_control) * 6.28 - _Time.y)) * 2 * (1 - 0.55 * (foam_color * depthEdge * _Foam_edge_control));//获取浮沫,因为越靠近岸深度值越小,泡沫颜色却要最白,所以要1减去和深度相乘的数值
foam_value = smoothstep(0.35, 1, foam_value);//去掉一些杂波
感觉这样的效果不太行,所以换成噪声贴图来实现(这里是实现后面一些效果后重新回来修改的):
//岸边泡沫
float3 viewPos = i.viewVec * depth01;//乘以线性深度
float4 r_under_position_WS = mul(unity_CameraToWorld, float4(viewPos, 1));
float depth_D_WS_value = r_under_position_WS.y - i.position_WS.y;
float depthEdge = saturate(_DeptheyeEdge * depth_D_WS_value);//得到线性的真实深度差值,并将深度差值调整到合适的范围内(用一个系数_DeptheyeEdge进行调整后取[0,1]内的数值)
//实现第一层
float2 foam_noise_uv = i.uv * _Foam_noise_Tex_ST.xy + _Foam_noise_Tex_ST.zw;//采样噪声贴图
half4 foam_noise_color = _Foam_noise_Tex.Sample(sampler_Foam_noise_Tex, foam_noise_uv - _Time.y * 0.05);//获取噪声贴图颜色
float foam_value = saturate(foam_noise_color * (1 - depthEdge) * _Foam_edge_control);//获取浮沫,因为越靠近岸深度值越小,泡沫颜色却要最白,所以要1减去和深度相乘的数值
//float foam_value = saturate(sin((1 - foam_noise_color * depthEdge * _Foam_edge_control) * 6.28 - _Time.y)) * 2 * (1 - 0.75 * (foam_noise_color * depthEdge * _Foam_edge_control));//获取浮沫,因为越靠近岸深度值越小,泡沫颜色却要最白,所以要1减去和深度相乘的数值
foam_value = smoothstep(0.45, 1, foam_value);
foam_value = foam_value > 0.6 ? 1.0 : 0;//这里用一个遮罩来判断,大于这个设定值取1,不然取0,就不会有太多的杂色,只有白色部分
//foam_value *= depthEdge;//让浮沫在岸边逐渐暗淡消散
//下面实现第二层的一个水体泡沫
float depthEdge1 = saturate(_DeptheyeEdge1 * depth_D_WS_value);
float2 foam_noise_uv1 = i.uv * _Foam_noise_Tex1_ST.xy + _Foam_noise_Tex1_ST.zw;//采样噪声贴图
half4 foam_noise_color1 = _Foam_noise_Tex1.Sample(sampler_Foam_noise_Tex1, foam_noise_uv1 - _Time.y * 0.01);//获取噪声贴图颜色
float foam_value1 = saturate(foam_noise_color1 * (1.0 - depthEdge1) * _Foam_edge_control1);
foam_value1 = smoothstep(0.45, 1, foam_value1);
foam_value1 = foam_value1 > 0.6 ? 1.0 : 0;
foam_value1 *= depthEdge1;
foam_value += foam_value1;
5.水体反射
这里使用平面反射,可以参考我写的另外一篇文章:
[Unity/URP学习]平面反射(PlanarReflection):https://editor.csdn.net/md/?articleId=128762625
记得将要反射的物体放在定义好的渲染层里:
我们还要对反射进行扰动,这里利用片元对应插值顶点的xz方向来扰动uv,对反射图像进行扰动。
half4 reflection_map_color = _Reflection_camera_RT.Sample(sampler_Reflection_camera_RT, i.screenPos.xy / i.position_CS.w + _Reflection_Distort * normal_WS.xz);//采样反射纹理贴图
6.水体折射
这里直接进行水底的扰动,直接对渲染完不透明物体的Camera Opaque Texture的uv进行扰动即可实现。
首先采样_CameraOpaqueTexture,直接返回采样结果,看一下显示结果:
//采样_CameraOpaqueTexture
half4 refraction_map_color = _CameraOpaqueTexture.Sample(sampler_CameraOpaqueTexture, i.screenPos.xy / i.position_CS.w);
这张图就是用来作扰动处理的一个图。
扰动效果的实现也很好实现,用当前片元屏幕坐标加上一个偏移值(用世界空间的法线xz扰动就可以)
//计算折射贴图扰动的uv偏移量
half2 Refraction_Distortion_UV(float3 normal_WS)
{
half2 distort_uv = normal_WS.xz * _Refraction_distortion_uv_control;
return distort_uv;
}
效果图:
会有很不错的偏移,但有一个问题,因为只是要偏移水下的物体,这样在水上物体和远处水体的交界会出现采样错误的现象:
修正
为避免上面的采样结果出现,我进行了修正,具体方法是:
假设上图箭头指的点是A点,那么我们做一个判断,如果一个点uv偏移之后,新的位置的片元对应的深度值>此新片元对应的水体插值点的深度(屏幕空间深度),说明这个新的片元采样的地方是在水体上方,那么这个时候就可以选择不偏移,就可以避免采样错误。像上图A点偏移后采样深度就小于片元对应水体深度,则取消偏移。
//计算折射贴图扰动后的uv
float2 Refraction_Distortion_UV(float3 normal_WS, float3 screen_pos)
{
float depth_water = screen_pos.z;
depth_water = LinearEyeDepth(depth_water, _ZBufferParams);
float2 distort_uv = normal_WS.xy * _Refraction_distortion_uv_control;//计算偏移值
float depth_new = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, screen_pos.xy + distort_uv).r;//计算新的片元对应深度
depth_new = LinearEyeDepth(depth_new, _ZBufferParams);
if(depth_new <= depth_water)//说明新采样点在水上,返回原uv
{
return screen_pos.xy;
}
return screen_pos.xy + distort_uv;
}
片元着色器:
//采样_CameraOpaqueTexture实现折射效果
float2 distortion = Refraction_Distortion_UV(normal_WS, i.screenPos.xyz / i.screenPos.w);//获取折射扰动后的uv
half4 refraction_map_color = _CameraOpaqueTexture.Sample(sampler_CameraOpaqueTexture, distortion);
但又有个问题,因为有一定范围的偏移会采样到水上物体,所以会有一小段明显的不扰动的边缘,因为另一边是扰动的,所以这两者会有明显分割线,如下:
但和反射进行菲涅尔计算之后,其实边缘不是特别明显,这里的修正等后面再来说说(也有可能是图的精度误差)
返回菲涅尔调整的反射和折射:
float fresnel = _Fresnel + (1 - _Fresnel) * pow(1 - dot(i.normal_WS, view_direct_WS), 5.0);//菲涅尔
half3 fresnel_color = lerp(refraction_map_color.rgb, reflection.rgb, fresnel);
注意菲涅尔用没有扰动的法线(i.normal_WS)计算!
7.水体焦散
焦散是指当光线穿过一个透明物体时,由于对象表面的不平整,使得光线折射并没有平行发生,出现漫折射,投影表面出现光子分散。焦散,俗称“水光”,波光粼粼—即指焦散现象。
因为焦散是发生在水下物体的表面,所以这里采用深度图世界坐标重建的方式获得水下物体点的世界坐标,然后用这个世界坐标的xz来采样(采样会取到小数部分(>0)来采样),采样如下:
//水体焦散
float2 caustics_uv = r_under_position_WS * _Caustics_Tex_ST.xy + _Caustics_Tex_ST.zw;
half4 caustics_color = _Caustics_Tex.Sample(sampler_Caustics_Tex, float2(-caustics_uv.y + _Caustics_Speed * _Time.y, caustics_uv.x));
在比较长的一个边缘处会有拉长的一个波纹,解决方法如下:文章来源:https://www.toymoban.com/news/detail-648785.html
最终效果:
最终
水体交互等内容后续补充。文章来源地址https://www.toymoban.com/news/detail-648785.html
到了这里,关于[Unity/URP学习]风格化水体渲染(一)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!