[UE4]动态液体材质浅谈
![](https://i-blog.csdnimg.cn/blog_migrate/e13795f7d1313e6001102402515707c4.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/98c1218b5ad82fca2d5689ab250e8813.jpeg)
大家好,我是练习UE4时长两年半的TA工具人。
最近的项目里大量使用UE4引擎。这引擎更新速度和幅度相当给力,今年发现两年半之前学的知识都已经过时了,很多东西又得重新捡起梳理。所以后面会更新一些UE4相关的内容,于我自己是学习笔记;至于你们怎么看我就不知道了。
这一次谈的内容是在UE4中实现 动态的液体材质,这个材质会实时识别和其他物体的交互,在交互区域根据需求改变流向,改变材质表现等等。最后的效果酷炫,在项目中可能使用概率较高。
这个方向网上能找到的资料相对稀少,并且现在能查到的做法(2019 0713)我并不是很认可。我觉得我的实现思路更加直观、清晰,可控性和可拓展性都优秀很多。
感觉不太正确的参考资料:
Distance Field Flow Map![图标](https://i-blog.csdnimg.cn/blog_migrate/15e8c80c086c202bef0ad367dcd3106d.jpeg)
https://www.youtube.com/watch?v=chPIKqa-0zk
使用版本:
UE4 4.22.3
涉及主要知识点:
DistanceToNearestSurface 节点、DistanceFieldGradient节点介绍与实战使用。
初等数学应用(主要就是向量)
水的基本流动
水材质我用的UE4自带的starter content里的M_Water_Lake,然后在这个简单材质上做了很多魔改。跟本文主题相关的内容后面会慢慢讲,关系不大的就跳过了。
![](https://i-blog.csdnimg.cn/blog_migrate/bb6cbfbf0364bb6b79b8e7e6625e1759.png)
![](https://i-blog.csdnimg.cn/blog_migrate/33855d1d5520c36cf5fa3a3bba8f1b5e.png)
水的基本流动原理非常简单,就是UV偏移。使用一个Panner节点就可以做出来了。下图是我用的,带有uvRepeat次数,以及可以调节流动方向的节点网。
![](https://i-blog.csdnimg.cn/blog_migrate/aaac0fbe9277a8aa215714929e6f7ae8.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/0c254ad23994109cd6ce15ec3b9b8bbe.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/f1f169f1f116594890f6be51f6b0c95f.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f66a3abda30946b5f46a4ac1703b7352.jpeg)
现在的水就可以根据我们定义的 (speedU,speedV)这个二维向量来决定水的流向,但现在还只是最基本的效果。
可以看出水在流动的过程中,和周围的物体完全不产生交互关系。后面我们要着手改进这一点。
水流受物体影响分析
![](https://i-blog.csdnimg.cn/blog_migrate/be0a8b45ee4fbc0b1e6818598019316e.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/228cbe6952ab9677e3e1ae4d78e9276e.jpeg)
目前我们的水体流动如图所示,纯粹是对贴图进行一个方向的偏移的结果。
如果我们想要水流和图中的圆球产生交互,最终水流应该是什么样的流向呢?想象一下,一定是绕着这个圆球继续往下流。
![](https://i-blog.csdnimg.cn/blog_migrate/8d4e17cd87e2bc581863c778c62d095a.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/778745bc2128982113d151153bde6fe7.jpeg)
如果我们只利用UV偏移的思路,怎么实现这样的效果呢。我们对比一下原来的流动轨迹 和 目标流动轨迹,来分析一下两者之间的关系。
![](https://i-blog.csdnimg.cn/blog_migrate/d8d97b2f100b3f3fd285bf97ecef0f48.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/b549c10c837ca0fdbbd27f5d58041508.jpeg)
如图示可以看出,大致上,原本的流向,按照球结构本身的点法线方向往外推,就可以实现我们的目标。
另外,随着离开球的距离越来越远,这种水流流向的改变效果应该会越来越弱。直到足够远之后,原本的流向完全不再改变,这才是符合自然规律的效果。
![](https://i-blog.csdnimg.cn/blog_migrate/3944a4b0a3abd3766aca1c2bd50f7ee2.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/58ae32678df08343fab51067893f765a.jpeg)
经过以上非常不严谨的推测,最终结论就是,我们需要获得两个信息:
1.球体与水面接触处的点法线方向
2.水面上的点的位置离球体的距离
distance field简单介绍
在UE4中已经有一个现成的节点,可以实时获取通过DistanceToNearestSurface 这个节点拿到。
按字面意思大致理解, 这个节点会返回到自己最近的模型上的点离自己的距离是多远。
这样说还是有点抽象,不好理解;本质上来说,这个节点计算的是距离场(distance field)。
距离场在图像处理领域,以及三维渲染领域用的都非常多,相关资料非常丰富。
现在我在SD里演示一下距离场的具体表现效果。使用节点 Bevel(本质是distance节点,也就是SD中算距离场的节点)
![](https://i-blog.csdnimg.cn/blog_migrate/edfe2cc621bf8b5fd98974e2a71a2419.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/46e73c2715a449e586354615b58b27c2.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/38c62e56ed206be11a7418af40a0364d.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/7f6016a163e33ac155a0e1aacb670038.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/8c3500c9fffafb27d0f2831b581f9083.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/4a91b7831b443afbf14d19c1253371a9.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/79a8b4272487939b8c0c7676324c29da.gif)
可以看出,距离场其实就用图像亮度 记录 某个点 离最近的图像边界的距离。
在UE4中拿取distance field相关信息
我们在材质面板常见一个DistanceToNearestSurface 节点,输入口提示需要输入position信息。这里我们只要传递进世界位置信息就可以用了。
输出口我们先挂在材质的自发光上观察这个节点计算出来的结果。
![](https://i-blog.csdnimg.cn/blog_migrate/74d8fcdeffe73f44b584b44effdd282f.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/779017106b249caba04b709cb91dc84d.jpeg)
使用一个面片赋予刚做的材质来观察效果。结果却一直是纯黑,看起来不对。为什么呢?
![](https://i-blog.csdnimg.cn/blog_migrate/e157b85b7287a139630ef21b5c4b5346.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/ec37f20612064a81ebb41e4f49fee211.jpeg)
这是因为UE4里要计算distance field,有一个全局的开关。这个材质节点只是拿取全局计算完的信息来用。如果开关没开,其实是没有计算信息的,就算创建了DistanceToNearestSurface 节点,也拿不到想要的信息。
所以先要在项目设置里开启 distance field 的计算:
Project Settings>Rendering>Generate Mesh Distance Fields
![](https://i-blog.csdnimg.cn/blog_migrate/0e218a22a166be95c17e7aa201128ec4.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/7c878833e0ee465c54ddfd8eb72b2fc8.jpeg)
开启功能重启编辑器之后,这个材质就有了效果:
![](https://i-blog.csdnimg.cn/blog_migrate/11bddf30b194afea60c8f3eec4999597.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/da599c9307e86d8ba05b8c2452cd70d6.jpeg)
因为计算出的数值非常大,我乘了一个非常小的系数。纯粹是为了观察。
![](https://i-blog.csdnimg.cn/blog_migrate/5f971f399ca87cefce0d1cb01c8913b4.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/a24c57c94980f8e0e6d5aa53daf5d58d.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/c56b4a38462f34e369616cd4e8c686f5.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/412f00aa24832ba302517e3e8d2e4027.jpeg)
通过DistanceToNearestSurface 节点,我们可以有自信说,可以拿到距离信息进行后续操作了。
但是相交位置的点法线信息怎么办呢?
UE4里还有另外一个节点可以干这个事,叫DistanceFieldGradient。
计算出来的结果如下图所示。这个节点是既带有方向又带有距离的,正好符合我们的需求
![](https://i-blog.csdnimg.cn/blog_migrate/d4d53ed790c596c67581c4096d1e4c4c.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/db9e7264d239cbb2e7a0c5e5f37770f1.jpeg)
DistanceFieldGradient节点初探
我们先连一套节点来测试一下,这个DistanceFieldGradient节点,在UV上的作用具体是什么样的。
原理很简单,就是用自己原本的UV减掉DistanceFieldGradient计算出来的向量,这样就可以做出沿着 相交区域点法线方向向外推贴图的效果。
下面节点中,normalize是官方推荐要连的。
UV是一个二维向量,我们只用DistanceFieldGradient节点的RG两个通道和UV进行相减计算。下面乘的pushAmount是控制效果强度的,0.01这个参数是让pushAmount不要太敏感,调参数的时候手感舒服一点,没有功能意义。
![](https://i-blog.csdnimg.cn/blog_migrate/f5f68ac859732f08f8e0d6a68ce56082.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/ccd1f1f73f412859ce7672b4220c4802.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/cebc1634e16a0371f2cbda7f74d93b4a.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/e8b3b4e9fea2ab05aff8f7d3f2700611.jpeg)
但是现在看起来有点问题,就是这个图被切成一个圆形了,外面的信息全没了。
这个问题我现在还没完全搞明白,感觉应该是DistanceFieldGradient这个节点本身计算有点问题,最外面的信息不完全是纯黑导致的。
现在尝试优化这个问题。
优化切割问题
优化的思路也比较简单直接。就是使用距离信息,大于某个指定距离值以外的向量,强行指定为(0,0)。
有了思路以后,我们就开始具体操作。首先我们要通过距离算出一个mask,这个mask的最外面是黑色,越往内越亮,直到纯白。
![](https://i-blog.csdnimg.cn/blog_migrate/b066603ccb6071a69c1db58ca15e7dce.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/411d33cd3ab22ed5eff14c30f54c3baf.jpeg)
这里就可以使用之前介绍过的DistanceToNearestSurface节点来获得距离信息了。因为默认算出来的距离是纯物理数值;也就是说,某个点离最近交点距离为4.13个单位远,那么该点的亮度就是4.13 。
如果我们想要指定的范围就是4.13,那么用DistanceToNearestSurface 节点计算出来的结果 除以4.13 ,那么4.13距离处的亮度现在就是1,从球表面到4.13距离处的亮度变化变为0 - 1了。
最后再加个1-x ,clamp掉超过1的亮度,就获得了我们想要的mask效果了。 最后加一个power是调节过渡的软硬程度的。
该部分节点如下
![](https://i-blog.csdnimg.cn/blog_migrate/1848ff00e5beba701b1ee4420101fb8b.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/2dc42f0986c6cd32fdd20c00ca8e04b2.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/3ea1867f43ad90996fc11cf6e39e5051.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/35528a14bdd71b883a2f25dd8395046f.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/352abe6731f43c8719a57f8e4c31cc91.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/7138ea530f1f6e0cf8c4c1b5ba787c06.jpeg)
再把做好的信息加上逻辑判断,mask等于0的亮度全部输出(0,0),大于0的亮度还是用原来的向量。
另外,按官方文档说DistanceFieldGradient是带有距离信息的,但是我感觉最后效果不明显,所以把DistanceFieldGradient计算出来的向量还乘了一次DistanceToNearestSurface的mask信息,这样就可以产生一个柔和的从内到外的过渡效果。
![](https://i-blog.csdnimg.cn/blog_migrate/3724793fa128993cfa5efe11d1be0091.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/1d71f825dbcd77d3b1b6360d9a7ee7e7.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/1756b85dd916489a87db8c2601c5ce4e.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/91fb9d3e7b1ceb92838b4ff5edb7a9f2.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/2b90e422c75a37cc9774b29b4ed13043.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/3d6d1adb699151fbe64b15a929a0b61d.jpeg)
但现在有个问题。感觉从周围绕开的效果太均匀了,和整体流动的大方向关系不明确。下面我们再尝试加入更多的方向性。
扰动带有方向性
原理依然非常简单—— 在原来的扰动向量基础上,再加一个方向向量。
这里我们使用的方向向量就是水流的流动方向,可以直接使用之前定义好的那个控制方向的二维方向向量。
这个向量第一次乘的是之前的distanceMask,第二次乘的是我们要控制的强度,然后直接加上去就好。
![](https://i-blog.csdnimg.cn/blog_migrate/eef7de09fb9bd660748855736226c101.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/5b220f53435a930d8ba4a23ba8ba3aef.jpeg)
调节这个向量影响程度的过程中,可以看到,扰乱的效果出现明显的拖尾。目标达成。
![](https://i-blog.csdnimg.cn/blog_migrate/e05697dd3fba21601305063f866a0992.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/c72152f3cfa99e73453e2fac2043fd6c.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/399107be2ea9f03b3a0f3784739fa1af.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/aabc4efa9948116e81b2165c9ac9324e.jpeg)
最后一个问题,非世界空间!
以上大体的效果上的东西,我们做得差不多了。但是其实这里还遗留了一个很大的坑。
我们到目前为止测试的效果都是在平面上的,如果把这些片放倾斜,效果就会出毛病!
![](https://i-blog.csdnimg.cn/blog_migrate/30bdf7840fa17d9a6b435c266a94a49f.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/5e0f531186bb488daaa7c6644ddac59a.jpeg)
怎么忽然就做出了个螺旋丸了呢???
其实问题是这样的。之前我们计算的DistanceFieldGradient结果是一个三维向量,代表的是距离场在世界空间里的朝向。而前面我们偷懒直接提取的RG两个通道当UV来用。
如果面片正面朝天,那么RG两个通道正好代表的XY轴向正好和UV的方向重合,只有在这种特殊巧合下,前面的做法才成立。而一旦面片进行了旋转,这个巧合就被粉碎。螺旋丸就产生了。
![](https://i-blog.csdnimg.cn/blog_migrate/7b33364bfc70c7912278dd97a271ba6a.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/abeeb6bf144d141e0cbbbc33117abf04.jpeg)
所以现在我们要想办法把所有的计算都拿到真正的世界坐标下去计算。
如下图所示,假设任意一个DistanceFieldGradient节点计算出来的向量(x,y,z),直接拿取它的xy作为UV偏移量来算显然是错的。
真正能用做UV偏移量的向量,应该是(x,y,z)向量在U方向(红轴)的投影长度,和V方向(绿轴)投影长度,组合而成的二维向量。
UV二轴原本单位向量分别为(1,0) 和(0,1),这个向量是切空间的(tangent space),不能直接拿来和世界空间的向量进行运算。
想要对二者进行计算,我们就需要将切空间的向量转换成世界空间的向量。
![](https://i-blog.csdnimg.cn/blog_migrate/4fa0a8bf4ed23504007bbdc09d9666eb.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/d60f58f3b85dee89ba44061dc387ddb0.jpeg)
UE4中有一个节点TransfromVector(Tangent Space to World Space)就是做这个事情的。(搜索transform创建的就是这个节点)
这个节点输入的是三维向量,所以我们创建一个(1,0,0)和(0,1,0)来代表切空间的UV单位向量,再通过TransfromVector将他们转到世界空间。
转换完成之后,再和给定向量(x,y,z)进行点乘,计算出来的两个分量合在一起的二维向量就是真正可以使用的UV偏移向量。
之前我们用if节点算完的结果就是我们上面假定的那个向量(x,y,z),这部分节点的组织方式如下图。
![](https://i-blog.csdnimg.cn/blog_migrate/4a1a549b41281b35dbfe8e4bb8c0a976.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/be89c3b8d1763f73e31ca6d7ce42f61f.jpeg)
当然这样一改,会有部分报错,原因是三维向量和二维向量不能放在一起计算。我们将之前某些用二维向量的地方全部改成三维向量就好了。
![](https://i-blog.csdnimg.cn/blog_migrate/02083019f7d57a733f553dec58a5ae1a.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/40c82e493452429c8bae9b27a4f925eb.jpeg)
做完的一套节点大概如图:
![](https://i-blog.csdnimg.cn/blog_migrate/3619b9a9c64c3eb424bd812861bd2ce9.jpeg)
最后,在一个球体上,无论什么角度,都不会出错。
![](https://i-blog.csdnimg.cn/blog_migrate/6ff74302d54eb4f08467c38d13eff07e.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/ce1a15599b57c1f0aef0bf9c74afdfbb.jpeg)
最后
写完了,花了好多时间。
这篇文章也只是介绍了一个大框架,具体想要更好的效果还有非常多可发掘的地方。
比如用算出来的距离信息去做一些置换什么的,用数学方法算出一些mask,决定哪些地方会出现泡沫等等。
我自己在项目里的运用比文章里写的这些内容也是复杂很多倍。
开篇发的两个链接里的做法,通过一些数学方法重算了近似点法线方向的信息,但严格上来说,那个信息是不准确的;虽然从最后的结果上看,是没有问题。但是在后续做更复杂的效果的时候,他们那样算出来的信息不准确,不利于后续计算。我是觉得那样做的可拓展性不够。
这点大家可以一起讨论一下。
整篇下来其实我自己觉得唯一有意思的部分是最后空间转换的思路,这个没查到资料,是我自己忽然顿悟想出来的。那种感觉太爽了。
这也是在UE4里做材质有意思的地方吧。
![](https://i-blog.csdnimg.cn/blog_migrate/e13795f7d1313e6001102402515707c4.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/98c1218b5ad82fca2d5689ab250e8813.jpeg)