8

如何在UE4中做出涟漪的效果

 2 years ago
source link: https://blog.uwa4d.com/archives/USaprkle_UEripple.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

涟漪这个效果我相信很多人都尝试实现过,也有各种实现方法。在这里,我实现的方法是使用Custom节点,用算法生成法线。接下来向大家分享一下思路,看一下最终效果图。文末提供了材质球百度云链接。

1.gif
最终效果图

简单地说一下原理:先用UV做出伪随机的格子,每个格子就是一个单独的UV,不过具有不同的灰度值,然后在格子的中心生成多个不同大小的同心圆,再做缩放和边缘混合。

随机噪波生成

首先,我们先定义一个三维向量用来进行三层不同大小涟漪的计算,因为UV的取值范围是0-1,所以我们定义的float3的值必须在0-1之间。这个值是一个系数,并不是实际的大小:

float3 ripple_scale3=float3(0.1,0.2,0.3)

然后,我们需要生成带有不同的灰度的UV格子:

float3 p3 = frac(float3(p.xyx) * ripple_scale3);
p3 += dot(p3, p3.yzx +20);
return frac((p3.xy + p3.yz) * p3.zy);

p是一个二维的向量,为了能和ripple_scale相乘,所以我们可以随便取它的.XYX或者.XYY。p3则是一个累加值,最后返回的值则是一个float2,因为UV是float2,随便取两个轴进行上述的运算就可以。然后我们定义一个一维的向量再次进行如下运算:

float ripple_scale1 = 0.1;
float3 p3 = frac(float3(p.xyx) * ripple_scale1);
p3 += dot(p3, p3.yzx + 10);
return frac((p3.x + p3.y) * p3.z);

上述的两个运算主要是为了得出一个足够随机的值,也可以用其它算法替代。我们如果将UV tiling 10次,然后floor之后作为上面代码中p的值,先进行三维的运算然后和下面一维的相乘可以得到如下结果(只要得到类似如下结果的算法都可以):

2.png

这个算法我们需要将其作为一个function,因为需要循环计算,所以得用一个strcut结构体进行声明后调用。

float ripple_scale1 = 0.1;一维随机数种子
float3 ripple_scale3 = float3(0.1, 0.11, 0.09);//三维随机数种子
float max_radius = 1;
struct rain
{

    float ripple1(float2 p)
    {
        float3 p3 = frac(float3(p.xyx) * ripple_scale1);
        p3 += dot(p3, p3.yzx + 10);
        return frac((p3.x + p3.y) * p3.z);
    }

    float2 ripple2(float2 p)
    {
        float3 p3 = frac(float3(p.xyx) * ripple_scale3);
        p3 += dot(p3, p3.yzx +20);
        return frac((p3.xy + p3.yz) * p3.zy);

    }
};
rain ra;

涟漪形状生成

接下来就是通过随机值产生涟漪并且动起来,这步需要循环采样,先把需要用到的变量声明一下:

float tiling = 10;//UVtiling次数
float2 uv = (UV) * tiling;//UV
float2 p0 = floor(uv);//floor之后会产生tiling个数长宽的UV格子
float i = 0;//x轴循环次数
float j = 0;//y轴循环次数,因为UV是双轴的所以有两个方向
float2 pi = 0;记录每个UV格子的不同灰度
float2 circles = 0;//圆圈
float2 p = 0;//初始位置

再准备循环体,把pi放入循环体进行累加:

for (j = (- max_radius);j <= max_radius; j++)
        for (i = - max_radius; i<= max_radius; i++)
        {
          pi = p0 +float2(i, j);
         }
     }

由于上述累加结果过大,我们将其进行除以tiling次数方便观察,很明显每个格子已经有了不一样的灰度值,因为i、j的值一直在累加。

3.png
pi循环后的值

然后我们在pi下面将pi的值代入三维的随机数function里进行运算:

for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
          pi = p0 +float2(i, j);
          float2 hsh = ra.ripple2(pi);//第一次随机运算
         }
     }


4.png
hsh循环后的值(随机化)

有了第一次就有第二次,我们继续将hsh的值再代入三维的随机值进行二次随机化,然后和pi的值加起来就能得到带有pi(UV位置信息的随机值),也就是我们的p。

for (j = (- max_radius);j <= max_radius; j++)
    {
        {
          pi = p0 +float2(i, j);
          float2 hsh = ra.ripple2(pi);//第一次随机运算
          p = pi + ra.ripple2(hsh);//得到带位置信息的随机值
         }
     }


5.png
p的值,同样便于观察除以了tiling值

然后,我们需要定义一个时间值t,同样需要随机化,但是是用一维的随机化函数(如果继续用三维会产生tiling,参考下图(下图中的tiling值为20)),然后frac做0-1循环。

for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
            pi = p0 +float2(i, j);
            float2 hsh = ra.ripple2(pi);//第一次随机运算
            p = pi + ra.ripple2(hsh);//得到带位置信息的随机值
            float t = frac(0.3 * iTime + ra.ripple1(hsh));//随机时间产生,0.3为速度,可在外部调整
        }
    }


6.gif
只采用一种随机化的t

7.gif
采用两种随机化的t

然后我们需要得出实际的位置,我这边用v表示,只需要减去UV值就行了,因为需要和UV对应起来。如果不减,我们最后将得不到法线(平的):

for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
            pi = p0 +float2(i, j);
            float2 hsh = ra.ripple2(pi);//第一次随机运算
            p = pi + ra.ripple2(hsh);//得到带位置信息的随机值
            float t = frac(0.3 * iTime + ra.ripple1(hsh));//随机时间产生,0.3为速度,可在外部调整
            float2 v = p - uv;//实际位置信息
        }
    }

接下来就是计算圆了。圆的计算公式是length(position)-R,其中position是圆心在UV中的位置,R是圆的半径。我们这边是max_radius+1,如果不加1,所有值都会比原来的小,这样会导致法线强度太弱,然后乘以我们得到的t就可以产生扩散的圆了:

for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
            pi = p0 +float2(i, j);
            float2 hsh = ra.ripple2(pi);//第一次随机运算
            p = pi + ra.ripple2(hsh);//得到带位置信息的随机值
            float t = frac(0.3 * iTime + ra.ripple1(hsh));//随机时间产生,0.3为速度,可在外部调整
            float2 v = p - uv;//实际位置信息
            float d = length(v) - (max_radius + 1) * t;//计算圆
        }
    }


8.gif
计算出来的扩散圆

但是因为这个只是其中一部分,圆形状的累加我们等会再做,先把涟漪的形状做出来。其实很简单,将得到的d进行sine函数运算一下就能得到涟漪,将d和一个值相乘能得到不同圈数的涟漪,然后用smoothstep控制涟漪的边缘虚实效果。我们这边要做两层,用两层的插值来模拟渐变,用h来控制涟漪的偏移值。

for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
            pi = p0 +float2(i, j);
            float2 hsh = ra.ripple2(pi);//第一次随机运算
            p = pi + ra.ripple2(hsh);//得到带位置信息的随机值
            float t = frac(0.3 * iTime + ra.ripple1(hsh));//随机时间产生,0.3为速度,可在外部调整
            float2 v = p - uv;//实际位置信息
            float d = length(v) - (max_radius + 1) * t;//计算圆
            float h = 1e-3;//就是0.001
            float d1 = d - h;
            float d2 = d + h;
            float p1 = sin(31. * d1) * smoothstep(-0.6, -0.3, d1) * smoothstep(0., -0.3, d1);
            float p2 = sin(31. * d2) * smoothstep(-0.6, -0.3, d2) * smoothstep(0., -0.3, d2);
        }
    }


9.png
d*30

10.png
d*60

11.gif
p1的效果

涟漪渐变效果

能动起来后,我们需要一个到达最大值后渐隐的效果,通过两者的差值乘以时间的反向,即1-0来模拟边缘的渐变效果,乘以两次时间是为了增强对比度。

for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
            pi = p0 +float2(i, j);
            float2 hsh = ra.ripple2(pi);//第一次随机运算
            p = pi + ra.ripple2(hsh);//得到带位置信息的随机值
            float t = frac(0.3 * iTime + ra.ripple1(hsh));//随机时间产生,0.3为速度,可在外部调整
            float2 v = p - uv;//实际位置信息
            float d = length(v) - (max_radius + 1) * t;//计算圆
            float h = 1e-3;//就是0.001
            float d1 = d - h;
            float d2 = d + h;
            float p1 = sin(31. * d1) * smoothstep(-0.6, -0.3, d1) * smoothstep(0., -0.3, d1);
            float p2 = sin(31. * d2) * smoothstep(-0.6, -0.3, d2) * smoothstep(0., -0.3, d2);
            circles = (p2 - p1) / (2. * h) * (1. - t) * (1. - t);//渐隐效果
        }
    }


12.gif
达到最大值后渐隐效果

涟漪形状累加

然后我们乘以它原来的normalize后的position(即v),即可得到现在的正确的法线效果,最后将每次循环的结果累加起来就可以得到我们想要的涟漪,再乘以数值可以控制法线强度:

for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
            pi = p0 +float2(i, j);
            float2 hsh = ra.ripple2(pi);//第一次随机运算
            p = pi + ra.ripple2(hsh);//得到带位置信息的随机值
            float t = frac(0.3 * iTime + ra.ripple1(hsh));//随机时间产生,0.3为速度,可在外部调整
            float2 v = p - uv;//实际位置信息
            float d = length(v) - (max_radius + 1) * t;//计算圆
            float h = 1e-3;//就是0.001
            float d1 = d - h;
            float d2 = d + h;
            float p1 = sin(31. * d1) * smoothstep(-0.6, -0.3, d1) * smoothstep(0., -0.3, d1);
            float p2 = sin(31. * d2) * smoothstep(-0.6, -0.3, d2) * smoothstep(0., -0.3, d2);
            circles = (p2 - p1) / (2. * h) * (1. - t) * (1. - t);//渐隐效果
            circles = 0.5 * normalize(v) * ((p2 - p1) / (2. * h) * (1. - t) * (1. - t));//得到正确法线方向
            circles=circles+circles;//效果累加
        }
    }


13.gif
circles法线累加效果

有了这个,我们法线的形状对了,但是效果不美观。因为是累加起来的,所以循环结束后除以循环总次数,即可得到正确的效果:

circles /= float(max_radius*2+1)*(max_radius*2+1);


14.gif
颜色矫正后的效果

生成法线

最后用求法线B通道的方式(开平方)求出B通道输出即可,为什么用点积做平方,我想大家都懂:

float3 n = float3(circles, sqrt(1. - dot(circles, circles)));


15.gif
法线效果

下面是完整代码:

float3 ripple_scale= float3(0.1,0.2,0.3);
float max_radius = 2;
struct rain
{

    float2 ripple(float2 p)
    {
        float3 p3 = frac(float3(p.xyx) * ripple_scale);
        p3 +=p3;
        return frac((p3.xy + p3.yz) * p3.zy);
    }
};
rain ra;

float tiling = 10;
float2 uv = (UV) * tiling;
float2 p0 = floor(uv);
float j = 0;
float i = 0;
float2 pi = 0;
float2 circles = 0;
float2 p = 0;
for (j = (- max_radius);j <= max_radius; j++)
    {
        for (i = - max_radius; i<= max_radius; i++)
        {
            pi = p0 +float2(j, i);
            p = pi+ra.ripple(pi);
            float t = frac(iTime + ra.ripple(pi));
            float2 v = p - uv;
            float d = length(v) - (max_radius + 1) * t;
            float h = 0.01;
            float d1 = d - h;
            float d2 = d + h;
            float p1 = sin(30 * d1) * smoothstep(-0.6, -0.3, d1) * smoothstep(0., -0.3, d1);
            float p2 = sin(30. * d2) * smoothstep(-0.6, -0.3, d2) * smoothstep(0., -0.3, d2);
            circles += 0.5 * normalize(v)* ((p2-p1 )/(2. * h) * (1. - t) * (1. - t)) ;
        }
    }
circles /= pow((max_radius*2+1),2);
float3 n = float3(circles, sqrt(1. - dot(circles, circles)));
return n;

水底石头生成

石头部分直接用Parallax就可以了,如果看过我前面文章的朋友可以用我前面改过的算法:
《如何在UE4中用raymarch实现面片水体(采样贴图)》

16.png
POM石头的高度图

反射

水面反射依旧使用Reflection Vector,我们最后的法线就是输入到这个normal接口,我这边用另一张法线和上面的ripple做了min让圆圈稍微产生了些变化:

17.png

18.gif
输出到Reflection Vector的结果

折射

对于折射,我这边是将上面的法线混合结果直接加到石头颜色的UV上就可以模拟了,当然强度得小一些。

19.png
折射和反射

透明度

最后用菲涅尔做出深度和透明度的变化就可以了:

20.png
菲涅尔制作深度和透明度

21.png
完整节点

材质球的百度云链接:
链接:https://pan.baidu.com/s/1xXvxxsYBjBVVnb42wOWFrA
提取码:fp37

这是侑虎科技第1021篇文章,感谢作者落月满江树供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者主页:https://www.zhihu.com/people/luo-yue-man-jiang-shu-38,再次感谢落月满江树的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK