[Unity/URP学习]风格化水体渲染(一)

这篇具有很好参考价值的文章主要介绍了[Unity/URP学习]风格化水体渲染(一)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

风格化水体的实现内容


着色:水体颜色、水体反射、水体折射、岸边泡沫、水面于天空沿边线消除、水体焦散
动画处理:水体流动、顶点动画、水体交互、水体浮力

风格化水体实现过程

(实现顺序没有严格按照着色和动画处理的分类来实现)

1.水体颜色

要制作水体颜色,要考虑的内容如下:
风格化的水体渐变颜色、水体深浅区域(进行颜色的渐变)

1.1风格化水体颜色渐变

可以到以下的网址自定义渐变色(自定义的使用cos的自定义渐变函数),并获取代码:
https://sp4ghet.github.io/grad/
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
因为有时无法选择其他语言的代码,也可以根据数据进行自行修改。
下面是渐变颜色函数的代码:

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的管线配置文件中开启深度纹理
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
勾选Depth Texture后在Frame Debug中可以看到CopyDepth这一步,就是将相机的深度纹理缓存,同时打开Opaque Texture是因为其保存了CopyColor,是将已经绘制的不透明物体的缓存,后面会用到。
unity水体,unity,学习,游戏引擎
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)

unity水体,unity,学习,游戏引擎
然后在Pass进行混合因子的混合:

unity水体,unity,学习,游戏引擎
关于URP混合的操作可以参考文章:
https://zhuanlan.zhihu.com/p/137404735?utm_campaign=&utm_medium=social&utm_oi=1128416725900914688&utm_psn=1603511076779155456&utm_source=qq
控制视图摄像机的距离远近,可以看到由深度引起的一个透明度以及渐变色的一个变换(进入game模式主摄像机也可以):
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎

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);

脚本调节面板:
unity水体,unity,学习,游戏引擎
效果图
unity水体,unity,学习,游戏引擎

可参考文章:
生成噪声图:
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,具体的内容如下图所示:
unity水体,unity,学习,游戏引擎
所以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方向流动的一个效果,返回值是噪声的颜色值),这里有了一些水面波纹的效果
unity水体,unity,学习,游戏引擎
近处因为像素间的颜色相差比较小,所以法线扰动不明显,远处的话扰动就比较明显了。
但感觉效果不是很好,下面基于此方法,再使用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&timestamp=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
                ...
            }

进一步扰动后我们可以调节噪声图扰动强度,以及在脚本面板中调整噪声图的生成各属性来获得更好的一个自定义流动效果(代码返回的是贴图采样的流动结果):
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎

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;

unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
在此基础上,我决定再采样一张水面波纹纹理来进行叠加扰动:
顶点着色器:

				//进行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));

unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎

3.顶点动画

3.1Sin叠加波浪效果

正弦波是最基本用于模拟海浪形状的波形,用于改变顶点位置,我们要计算它在各个方向上的顶点位移和法线。
实现之前,先定义波函数相关参数:
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎

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)

切线向量

推导如下:
unity水体,unity,学习,游戏引擎

副切线向量

推导如下:
unity水体,unity,学习,游戏引擎

法线向量

将副切线向量和切线向量进行叉乘:
unity水体,unity,学习,游戏引擎
到这里就成功获取顶点动画中的顶点的新法线。

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;//获取新法线

换了一个场景实现:
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎

3.2Gerstner波

Gerstner波描绘的是在水面上下波动的同时,xz也会偏移。在水面上的一点接近峰值时,会向着波峰移动,波峰消散时会向着相反的方向移动,实现波峰更加尖锐,波谷更加平坦,具体如下图所示:
unity水体,unity,学习,游戏引擎

3.2.1顶点位移

曲面方程为:
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
注意:Qi是坡度,Qi为0的时候是正弦波,Qi为1/(wi * Ai)时得到一个尖峰波形
unity水体,unity,学习,游戏引擎

3.2.2法线计算

计算过程和Sin波类似,这里给出计算之后的切线、副切线、法线的向量:

unity水体,unity,学习,游戏引擎
对比之前的浪(这里夸张处理了一下):
Sin波:
unity水体,unity,学习,游戏引擎
Gerstner波:
unity水体,unity,学习,游戏引擎
很明显,波峰波谷的效果要好很多
下面是调整好的图:
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
叠加了多个波后:
unity水体,unity,学习,游戏引擎
多个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,起点为相机原点,终点是在远平面上的一点
下面产生边缘的计算图:
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎

片元着色器:

				//边缘产生
                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),效果如下:
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎

调整系数后就可以产生非常稳定的边缘。
参考文章:
【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减去和深度相乘的数值

unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
加入smoostep调整去掉一些杂波:

foam_value = smoothstep(0.4, 1, foam_value);//去掉一些杂波

unity水体,unity,学习,游戏引擎
最终效果:
unity水体,unity,学习,游戏引擎

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);//去掉一些杂波

unity水体,unity,学习,游戏引擎
但缺点很明显,并没有实现白沫消散的过程,而是直接就分成有白沫和没有白沫的两段。
如果代码修改如下:

float foam_value = saturate(sin((1 - foam_color * depthEdge * _Foam_edge_control) * 6.28 - _Time.y));

unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
调整过后会有接续的白沫带产生,但是外海区域(0)会由于时间叠加而周期性变白,原因是:在之前计算的depthEdge是[0,1]的范围,而外海区域是0,所以1 - foam_color * depthEdge * _Foam_edge_control始终是1,和_Time.y(决定流动方向而已)进行相减后,横坐标变换,原来是0的sin取值就不再是0,而是在[0,1]之间波动,就会有周期性的一个变白的过程,所以思考之后的解决方法如下:
设置一个遮罩,让第二条波纹产生时不是整个外海都变白色,而是只有第二条波纹的边缘处是白色,而且要从无到有的缓慢产生,所以考虑了采样的画图如下:
unity水体,unity,学习,游戏引擎

当采样函数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减去和深度相乘的数值

unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
但是现在两条白沫带的频率比较低,会出现没有白沫带的时候,我们可以调节系数来得到我们的效果

				//岸边泡沫
                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);//去掉一些杂波

unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
感觉这样的效果不太行,所以换成噪声贴图来实现(这里是实现后面一些效果后重新回来修改的):

				//岸边泡沫
                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;

unity水体,unity,学习,游戏引擎

5.水体反射

这里使用平面反射,可以参考我写的另外一篇文章:
[Unity/URP学习]平面反射(PlanarReflection):https://editor.csdn.net/md/?articleId=128762625
记得将要反射的物体放在定义好的渲染层里:unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
我们还要对反射进行扰动,这里利用片元对应插值顶点的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);//采样反射纹理贴图

unity水体,unity,学习,游戏引擎

6.水体折射

这里直接进行水底的扰动,直接对渲染完不透明物体的Camera Opaque Texture的uv进行扰动即可实现。
首先采样_CameraOpaqueTexture,直接返回采样结果,看一下显示结果:

				//采样_CameraOpaqueTexture
                half4 refraction_map_color = _CameraOpaqueTexture.Sample(sampler_CameraOpaqueTexture, i.screenPos.xy / i.position_CS.w);

unity水体,unity,学习,游戏引擎
这张图就是用来作扰动处理的一个图。
扰动效果的实现也很好实现,用当前片元屏幕坐标加上一个偏移值(用世界空间的法线xz扰动就可以)

			//计算折射贴图扰动的uv偏移量
            half2 Refraction_Distortion_UV(float3 normal_WS)
            {
                half2 distort_uv = normal_WS.xz * _Refraction_distortion_uv_control;
                return distort_uv;
            }

效果图:
unity水体,unity,学习,游戏引擎
会有很不错的偏移,但有一个问题,因为只是要偏移水下的物体,这样在水上物体和远处水体的交界会出现采样错误的现象:
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
修正
为避免上面的采样结果出现,我进行了修正,具体方法是:
假设上图箭头指的点是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);

但又有个问题,因为有一定范围的偏移会采样到水上物体,所以会有一小段明显的不扰动的边缘,因为另一边是扰动的,所以这两者会有明显分割线,如下:
unity水体,unity,学习,游戏引擎
但和反射进行菲涅尔计算之后,其实边缘不是特别明显,这里的修正等后面再来说说(也有可能是图的精度误差)
返回菲涅尔调整的反射和折射:

				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)计算!
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎

7.水体焦散

焦散是指当光线穿过一个透明物体时,由于对象表面的不平整,使得光线折射并没有平行发生,出现漫折射,投影表面出现光子分散。焦散,俗称“水光”,波光粼粼—即指焦散现象。
unity水体,unity,学习,游戏引擎
因为焦散是发生在水下物体的表面,所以这里采用深度图世界坐标重建的方式获得水下物体点的世界坐标,然后用这个世界坐标的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));

unity水体,unity,学习,游戏引擎

在比较长的一个边缘处会有拉长的一个波纹,解决方法如下:

最终效果:
unity水体,unity,学习,游戏引擎
unity水体,unity,学习,游戏引擎
最终
unity水体,unity,学习,游戏引擎
水体交互等内容后续补充。文章来源地址https://www.toymoban.com/news/detail-648785.html

到了这里,关于[Unity/URP学习]风格化水体渲染(一)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Unity Shader】Plane实现风格化水

    写在前面 长文警告!!!!! 很久没更新博客了,,这次是要做一个风格化水效果,是基于Plane着色实现水面效果。 项目:Unity 2017.4.40f1 Build-in,因此实现过程会跟URP有些出入(例如获取相机深度图等等),但思路都是一样的。 以《RIME》 和《原神》为例: 想实现的是二者

    2024年02月08日
    浏览(39)
  • AI绘画风格化实战

    在社交软件和短视频平台上,我们时常能看到各种特色鲜明的视觉效果,比如卡通化的图片和中国风的视频剪辑。这些有趣的风格化效果其实都是图像风格化技术的应用成果。 MidLibrary 这个网站提供了不同的图像风格,每一种都带有鲜明的特色。 MidLibrary 它总共包含了以下几

    2024年01月20日
    浏览(41)
  • Stable Diffusion 对图像进行风格化

    风格化是基于现有图像转换成另一种风格的操作方法,通常应用于 img2img 中,将文字提示中特定的新风格应用于原图像上进行修改。在这个过程中并非使用随机的潜在状态,而是采用原始图像去编码初始潜在状态。在此基础上通过加入少量的随机性,这种随机性由去噪强度所

    2024年02月12日
    浏览(43)
  • MATLAB GUI的多种数字图像处理(图像风格化)

        本课程设计基于MATLAB GUI进行创建UI界面,并集成多种图像处理,包括图像模糊、图像锐化、BBHE直方图均衡化、图像素描、图像反色、图像浮雕、canny边缘检测、图像膨胀、图像腐蚀、图像油画功能,同时也给了保存图像的功能,经运行验证,UI界面可视化良好,图像处理

    2024年02月03日
    浏览(49)
  • 在 Blender 和 3DCoat 中创建风格化的幻想屋

    今天云渲染小编给大家带来的是CG艺术家Brian Nguyen 最近的项目“一个风格化的幻想屋”幕后制作,讨论了 Blender 中的建模过程和 3DCoat 中的纹理过程,并详细介绍了如何设置灯光和K动画。 我是 Brian Nguyen,程式化的 3D 艺术家,一个喜欢幻想和可爱、怪异、狂野和美好事物的人

    2024年02月11日
    浏览(88)
  • 生成式 AI 在泛娱乐行业的应用场景实践 – 助力风格化视频内容创作

    感谢大家阅读《生成式 AI 行业解决方案指南》系列博客,全系列分为 4 篇,将为大家系统地介绍生成式 AI 解决方案指南及其在电商、游戏、泛娱乐行业中的典型场景及应用实践。目录如下: 《生成式 AI 行业解决方案指南与部署指南》 《生成式 AI 在电商行业的应用场景实践

    2024年02月13日
    浏览(39)
  • 瑞云介绍使用ZBrush和Marmoset工具包制作的风格化巨怪战斗机

    Renderbus瑞云渲染的小编今天给大家介绍下Gianluca Squillace使用 ZBrush 和 Marmoset 工具包制作巨怪战士的一些技巧。这位艺术家还贴心地告诉大家,有些步骤是可以省略跳过的,这样就可以节省时间,帮助我们快速完成角色的创作啦。快速有用的步骤可以看看下文哦! Gianluca Squil

    2024年02月07日
    浏览(52)
  • Unity——URP & HDRP 渲染模式学习笔记

    目录 HDRP和UPR是两种完全不同的渲染模式,不能混用,只能选择其中一种,最好在项目刚开始的时候就确定好。否则后期所有模型的材质都要改变非常麻烦。 修改渲染模式方法: HDRP, URP, BRP区别 备注 project settins: 1. graphics-Scriptable render pipeline settings 要修改,假如从hdrp改为

    2024年02月04日
    浏览(48)
  • Unity-3DRPG游戏 学习笔记(1)--使用URP渲染管线

    教程地址: Unity2020 3DRPG游戏开发教程|Core核心功能01:Create Project 创建项目导入素材|Unity中文课堂_哔哩哔哩_bilibili 创建URP通用渲染管线(2021版本) 1. 打开:Windows--Package Manager--左上角下拉选择Unity Registry--搜索Universal RP--Install 2. Project窗口--Assets--右键Create--Rendering--URP Asstes

    2024年02月11日
    浏览(46)
  • 【unity shader】水体渲染基础-水下透视效果

    接下来是水体渲染基础的最后一篇,通过水面看到水下的物体,并呈现深度效果。 我们直接搭一个小场景。 增加水面,赋予uv变形的水面材质,并增加透明度的设置。 水体会吸收光线,所以真实的水体并不是完全透明的。此外,水体对不同频率的光吸收率不同,蓝光被吸收

    2024年03月14日
    浏览(56)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包