本文将讲述点云在虚幻引擎中的部分运用以及血管生长的特效的制作方法。
关注我的b站~
https://space.bilibili.com/198377617/?share_source=copy_link&share_medium=iphone&bbid=Y84C1A87238831C048E09014BFC2D3B481AF&ts=1587578403
![634acd9b00f3bfb1a028c791513a40db.png](https://i-blog.csdnimg.cn/blog_migrate/92d263658406ba36bebc588d318c19a0.jpeg)
项目链接,请多支持^_^:
https://www.unrealengine.com/marketplace/zh-CN/product/vein-growth-effect-point-cloud-function-library-in-blueprint
原视频网址,请多帮忙点赞^_^https://www.youtube.com/watch?v=p9u-j8suzDc
鄙人原来希望的是着重讲解一下点云的渲染以及形变,但是由于鸽了太久(抱歉),虚幻的新粒子系统Niagara出现了,渲染部分会变得异常简单,因此着重点变为在特效中的运用。
目录
零、动机一、点云的生成 1.生成 2.R2 Seqence二、最短路径算法Dijkstra三、桶排序
1.桶排序
2.针对kNN的优化四、kNN 1.暴力法 2.kdTree 3.VoxelGrid五、邻接矩阵的生成六、血管生长特效
七、问题与改进
零、动机
最近接触了一下Houdini,被其简单易用的节点吸引了。在网上看到了许多实用且有趣的特效,我开始思考,是否这些特效能够在一些游戏引擎中实现呢(指不使用任何插件)?毕竟算法是基本,特效也是建立在算法之上的。怀着这种想法,本人打算写下几篇,从Houdini到UE4的文章。这个系列不打算简单地使用插件来完成这些特效的转换,而是从底层原理出发,在UE中重新实现一次,并分享给读者们。全文讲述思路比较多,因为是本人个人的思路,还请斧正。
![8dfb64f096c9d70e3c173600d2d85182.png](https://i-blog.csdnimg.cn/blog_migrate/c33bad3619c5f6d0b93f71a2eb5ea1e2.jpeg)
一、点云的生成
Houdini中,VeinGrowth的特效是通过模型的顶点连接到终点的最短路径实现的。
具体在Houdini中的实现参考:https://www.youtube.com/watch?v=sonXI31wDxs&t=262s
因为在Houdini中模型均为高模(相对于游戏),因此点比较密集与均匀。考虑到游戏中的模型通常都是优化过的,因此“点”的构造显得十分重要。
1.生成
要想让点均匀分布在模型表面,并不是一件容易的事情。一开始最基本的想法都是在每个三角形上均匀撒点,最后得到的点集就是均匀的结果。但是这种朴素的思想会引来一个巨大的bug,想象一下,当两个表面贴合得很近的时候,两个表面生成的点,极有可能“贴合”在一点,导致分配不均匀。
因此这个“均匀”的定义就显得有二义性,是表面均匀还是空间均匀,需要进行考虑。如果是空间上的均匀,思路也很简单,用网格包裹模型,在经过网格的地方随机填入点,并投影在三角形上,便可以得到“空间均匀”的点。
当然这个操作的代价也是巨大的,一个是不能控制点的大概数目,因为网格的粗细会明显影响其数目,除非网格密度尽可能得大。其次是对于蓝图来说,复杂度太高,对运算不友好。
因此还是选择第一种表面均匀的方法,然后判断两个点是否靠得太近,来选择是否保留,来尽量控制空间上的分布。但是这里就会引入一个问题,这个“Fuse”的操作,已经铁定是
三角形内随机撒点(Uniform random point in triangle):
用到了Trilinear coordinates的方法
这里只给出公式,具体推导可以参考
https://en.m.wikipedia.org/wiki/Trilinear_coordinates
根据常识,对于面积大的三角形内应该多撒点,反之就少撒点,因此每个三角形的期望点数是:
其中
2.R2 Sequence
如果使用单纯的随机数作为输入,得到的点可能并不会那么“均匀”,这里需要引入low discrepancy sequence的概念。
最初想到的是
https://statweb.stanford.edu/~owen/reports/triangleqmc.pdf这个实现,Low discrepancy constructions in the triangle,后来觉得他太复杂了(实际上是看不懂括号删掉),于是找到了更纯粹的做法,仅修改两个随机输入,将随机输入换成low discrepancy的。实际上这么做会引入新的问题(具体是什么留给读者思考),但是,根据图形学第一定律,看上去是对的,就是对的。If it looks right,it is right.
最简单的low discrepancy可以说是R Sequence了,具体实现很简单:
对于二维的情况,第二项的
详细说明在:http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/
将以上功能合并,就得到了一个操作简单实现方便的伪·均匀的点云(只用蓝图)
为了看起来高端一点我把公式也写在这里好了:
![327894c9bc60ae4494f4288af9ed819d.png](https://i-blog.csdnimg.cn/blog_migrate/600797c484fbcbe9346553b4c730f89a.jpeg)
二、最短路径算法Dijkstra
这里只简单描述,不详细讲解该算法。
目标是寻找任意一点到终点的最短路径,实际上也是终点到任意一点的最短路径。之所以选择Dijkstra,是因为他也是单源最短路径算法,同时结果得到的path数组可以循环利用获得到所有点的最短路径,更重要的一点是,他可以轻松地在蓝图中实现(本文的基础是蓝图)
Dijkstra输入是一个邻接矩阵,邻接矩阵实际上就是两两距离矩阵的修改版,和上面所说的Fuse的过程有交集,不难想到可以在一个过程中完成这两件事。
最后得到n条最短路径,而其中许多路径是重复的,实际上是一棵树,剩下的只需要将这棵树渲染出来就完成任务了。
关于距离矩阵和邻接矩阵,为了让文章显得更加充实,加入几个公式(顺便练练Latex能力):
距离矩阵的行列下标代表点的序号,因为两两点的距离是固定的,因此他实际上也是一个对称矩阵或者说对角矩阵(而且对角线是0),通过这个性质也可以减少内存的使用(编码会稍微麻烦一点)
为了方便渲染,我们需要改造这种树和链表的数据结构,从path数组生成一棵树的图解如下:
拿一颗最简单树为例(拿MsPaint画几张简单的示意图)
![af7c71865662bfe6a1846701b95e0bb1.png](https://i-blog.csdnimg.cn/blog_migrate/920153211ad323b17d3497f08d756cb2.png)
![7130a411125cb5c61f6f24c6ffdcf2e4.png](https://i-blog.csdnimg.cn/blog_migrate/4212f73a6c6041e9f8739026ddcf8ab8.png)
![359d7ecab96bac4c23240c548c2ac681.png](https://i-blog.csdnimg.cn/blog_migrate/22e8d8641b697219bb2fe968f696dbdf.png)
这种结构的解耦貌似叫Separate Chaining,因为这不是重点而且写下文章的时候还没有去考究,这个说法暂时存疑,如果读者们知道专业术语的话还请在评论区中提出指正^_^。
三、桶排序与桶排序的优化
1.桶排序
点云的邻接矩阵的生成需要用到kNN,这个是接下来要讲的,而kNN中需要用到距离的排序,也就是Sort,在蓝图中使用快排插排并不太推荐,不仅难以连线,而且效率低下,如果说有一种排序效率高但是不精确,对于一个特效来说,不精确似乎没有什么所谓(但是后面可以证明在这个点云的生成模式下是无限接近精确的排序)。
这种排序方法就是桶排序。桶排序很简单,思想是空间换时间,把距离根据距离大小放到对应的桶中,最后将桶一一Append最后得到一个有序的结果。之所以说他不精确,是因为桶内无序,桶内元素顺序不定)但是由于点云是均匀分布的,只有在极其极端的条件下才会导致多个点到一个点的距离相似(比如球面到球心,这种情况基本不可能),再加上kNN和邻接矩阵的生成都只需要最近的点,因此是无限接近准确的排序结果,也是最容易实现的方法,在蓝图中。
首先需要用到一个结构体,该结构体作为一个个桶,来装元素,这个桶数组化后便可以桶排序装元素了:
![a98738151ad1bd38cd80f720eb043f8c.png](https://i-blog.csdnimg.cn/blog_migrate/b8c935634f2f16bb4030bf200df5b492.png)
![19e05e884957ba1fb7fb878b577a57ff.png](https://i-blog.csdnimg.cn/blog_migrate/f4e94785776856258c75d80682a55969.png)
具体实现请参考:https://en.wikipedia.org/wiki/Bucket_sort
2.针对kNN的优化
kNN的重点是,NN(Nearest Neighbor),既然是Nearest,说明远处的元素不重要,因此想到可以根据修改映射函数来达到这种效果:近距离(距离小)的情况,桶的密度高,远距离的就不管了。
可以使用简单的
![5b19a4a57f5f4989798e9369f4468047.png](https://i-blog.csdnimg.cn/blog_migrate/45e151ce31754470d9f5ec17c0477aa8.jpeg)
四、kNN(k-NearestNeighbor)
kNN中文为K最近邻,字面意思,找到k最接近的点。1.暴力法
本文采用的是暴力法,得到两两点的距离矩阵后,分别对每一行的元素进行排序,得到排序矩阵,对于取前面k列的元素,得到了全部点的kNN。用于后面计算邻接矩阵。2.kdTree
本文不详细解释,因为在蓝图里实现kdTree会有一点困难。3.VoxelGrid
网格法也十分简单,用2D的举一个例子:
![c0fe8914c02ffe036e53f0101258dc1d.png](https://i-blog.csdnimg.cn/blog_migrate/acf8fe3cc5fbac5314cb96a480d13dd1.png)
每一次迭代寻找更大的圈,直到找到k个元素为止。
在蓝图中实现过,但是由于每迭代多一次,循环次数就以立方倍递增,效果不太好(暴力法就只是两次历遍),但是这种算法不需要全部Sort,也是一种可供选择的方法。
五、邻接矩阵的生成
邻接矩阵可以看成是一个距离矩阵挖去没有连接的部分(标记为最大,因为该路径代价无限大),因此利用kNN得到的临近点数据可以计算出邻接矩阵。
这里有一个小坑,先思考一下,这点q的k临近点假如有p,但是p的k临近点可能并不包含q,这里蕴含的是这个邻接矩阵有无向的信息,如果连接pq,只要其中一方是另一方的邻接点,得到的矩阵就是无向图的邻接矩阵。反之,则是单向的邻接矩阵,具有方向性。这两种效果有这稍微的不一样,而且针对这个有一种简易运算的方法,将在后面分享。
总而言之基本思路就是,先取得距离矩阵,最后对于每一行中的每一个元素,判断是否在kNN的集合之中,如果不在,则标记为无穷大。(自身到自身的距离也是无穷大)
为了充实文章内容打上公式^_^:
六、血管生长特效
对邻接矩阵进行Dijkstra之后,得到一个path数组,将其转换为了一棵树。
这棵树储存了parent的信息,这样一段一段的信息都可以被读取出来。
剩下的任务就是渲染这一棵树。这里提供两种方法。1.SplineMesh(慢,方便)
最快的算法便是将每一段的起点顶点都喂给一个SplineMesh,而且要注意的是为了看上去连续,tangent必须连续,因此,parent的parent的位置作为起点的tangent(通过树结构可以轻易获得,注意判断是否是根节点或者首节点),终点的child也同理,应该作为终点的tangent。最后就得到了n条SplineMesh。此方法简单,但是十分消耗资源。
这里有个坑点是,所有的SplineMesh的UpVector需要被统一。
2.ProcedrualMesh(快,复杂)
基本思路是把SplineMesh转为ProcedrualMesh,转化一下问题就是放样,简单的SP曲线的放样,看上去很复杂,但是由于UE集成了Spline,这些操作也变得简单。
对于1中的所有Spline中的每一条,我们需要将其分割为n段,获取这n段中的UpVector
![824a5662b91bcb37fa4b772b67056f22.png](https://i-blog.csdnimg.cn/blog_migrate/23768385e621de48718812effbdefb01.png)
这里又迎来一个选择,是选择段与段的连接处共用顶点,还是选择就这样算了。前者我试过,为了矫正Twist,需要做很多工作(还不如自己重写个Spline),因此这里直接说结论,就这样不管了效果更好。
因为整个ProceduralMesh只有一个Mesh,比起SplineMesh效率提升很多倍。而且面数可调,极力推荐这种方法。
因为树结构隐含了层次顺序信息,利用这个信息可以轻松做出这种渐变生长的效果。
本文把Layer储存在UV信息中,随着时间的流动判断一次UV的值,选择部分渲染部分不渲染,便是血管(藤蔓)生长的特效。
![b8e700151644ba0ed3ee1f9ea72c0cd0.png](https://i-blog.csdnimg.cn/blog_migrate/66df85220d3ce0fdb90401e886861403.png)
七、问题与改进
这个算法有几个问题,本文指出其中的一部分。
1.路径并非完全贴合模型。这是因为点云的邻近点不一定在原模型中*共面造成的。()而这个缺陷可能造成的结果就是,可能出现飞线以及边角位置穿模。可以通过加强SPline的Tangent来减缓这种瑕疵。
2.算法复杂度过高。受限于蓝图,无法使用其他加速结构。这一点改进方法有很多,但是都没有蓝图有意思。
3.前面提到的在Fuse的过程同时获取邻接矩阵,获取的是有向的邻接矩阵,并不是对角矩阵,效果会比对角矩阵差。关于如何修正这个对称性,目前还没有想到好的办法。
和Houdini中原来的实现方法有什么不一样呢?原方法中ShortestPath是指模型表面的ShortestPath,如果完全复现,可能需要用到其他复杂的算法,蓝图里难以实现,因此将该距离转换为点云中的距离,实际上蕴含了既有表面重建的特征(kNN)来保证特效的正确,也成功引入了图论算法Dijkstra来保证效果正确。是一个比较成功的转换,个人认为。
那么这个文章的主要内容也到此为止了。
接下来的文章是,在蓝图中复现Houdini的Remeshing,可能需要一个月左右的时间去写一篇文章,要准备各种资料,还要考虑措辞严谨性嗯。。。。以前发文章还有以装逼为动力,现在发文章就突然觉得心累了QAQ,希望能有更多人看,大家可以一起讨论自己的心得。
![3f415316aab16142e7bec7725f6d115c.png](https://i-blog.csdnimg.cn/blog_migrate/7076f4df90538fe3acf07fdabd7524ce.jpeg)
![1a9f54a3bc790183eb88f70157939df6.png](https://i-blog.csdnimg.cn/blog_migrate/0c4897af398564979bdae7807eead3de.jpeg)
附,我的文章不是教程,我的所有文章都是在分享自己的思想,每一步怎么想的都完整呈现了出来,希望能够帮到初学者,也希望有大神可以指出不足,这也是我写这一类文章的目的。比起分享知识,分享思路举一反三可能会更好,本人是如此觉得的。
本文项目链接:
https://www.unrealengine.com/marketplace/zh-CN/product/vein-growth-effect-point-cloud-function-library-in-blueprint
顺便想说一个事情,实际上在这篇文章发布之前,该资源已经被我提交到Market上了。之前 也发布过不少的蓝图插件,但是在国内都发生了一些不愉快的事情。原本变现技术是一件很正常的事情,但是面对国内的市场却面对着一个情况,就是团购。说实话我也曾经团购过其他的商品,但是后来也都补票购入了许多资源(主要是为了商业用途)。至于团购这种事情对不对其实界限很模糊,能够得到一个肯定的结论是,团购者没有购买的凭证用作商业用途是违法的,但是用作学习,可能问题不大。但是在我最近发布完这个奶茶的插件,有一些群在我发布的1小时后(对,就是他妈的一小时)就在群里发布了团购的申请。我有点愤怒,却又愤怒不起来。错没错虽然是模糊的,但是我还是希望这种灰色地带的事情能够不要得寸进尺。如果我已经发布了很久,我得到了我应该有的收益,我觉得团购用来学习,没有问题。一小时,太过分了。这是严重损害我个人的利益的行为。不仅如此,b站上还有盗本人视频的行为,并且在描述上从来没有提起作者(本人),加上了许多“外包找xxx”等让人误会视频内容是转载者实现的错觉。既然国内环境如此,与其让插件暗中流动,不如由我主动写成文章分享出来,让更多人可以一起交流(已经不打算赚国人的钱了,反正最后百度网盘淘宝上都会是拷贝)。
![737be726efb340fe2140995fcf624280.png](https://i-blog.csdnimg.cn/blog_migrate/b8305fbec25ee043fee1727128aedbc1.jpeg)
这是原视频以及原发布地址https://www.youtube.com/watch?v=hHE8XDoPalg
这是Market地址https://unrealengine.com/marketplace/zh-CN/product/fantastic-milktea-liquid-simulator
等我把文章理清楚了我也会把这个的实现思路过程都分享出来。
还有一点,感谢社畜酱一直陪我Debug @社畜酱
关于转载,貌似只有 @小兔叽fancer 有征求过我的同意,因此其他之前的陈年老文在其他平台上的转载也和盗视频的一样举报了,删没删除不清楚。
恳求各位开发者尊重一下作者,本来已是用爱发电,遇到这种事情心凉一半,就这样吧。还请各位交流斧正点赞转发评论五连。
感谢阅读。
附录:引用与参考
Houdini的VeinGrowth
Trilinear coordinates
Low discrepancy constructions in the triangle
Lagged Fibonacci
R-Sequence
Bucket sort