【Aegisub】融球效果、粘性小球与液体水滴效果

这次又来讲一个没人用Aegisub做过的东西。下面这几个动图都是用aegisub做出来的,而且都是一个个大的绘图、并不是用像素堆出来的





是的没错,利用液滴连火焰效果都可以做,生成一张张随机的火焰图,并不是用一堆像素粒子堆的,所以预览起来特别爽啊!啊,我好爽啊,我不行了,太爽了,怎么这么爽啊!
那么这个要怎么做呢,先看下面这个动图

可以看到,两个本身没有融合效果的圆在进行模糊以后,就有了融合的感觉了,所以在此基础上调一下对比度就可以得到真正的溶球效果了。所以你可以自己试试,在aegisub里用高斯模糊,然后你拖动字幕定位点,就可以看到两个球有融合的感觉,不过当然调对比度就没办法了,所以你可能就会想那这个不就需要用像素来做了?不过,不要小瞧爱的力量啊!相信爱啊!!
现在假设每个物体对象都有一个场,并用一个场函数表示这个场。比方一个圆,假设一点距离场心越近场强就越强,那这个场看起来就可以是这样的:

或者你可以想象成离中心越近的地方就密度越大、所以颜色越深。好了,总之离场心越近场强就越强很简单,圆的方程是

其中(x0,y0)是圆的中心,(x,y)是圆上的点,r是半径,那么这个

大于等于1的时候,(x,y)当然就可以是圆上和圆内的任意一点了,并且(x,y)距离中心越近的时候,场强就越大,在场心的时候此时的场强就无穷大。而当然,如果你(x,y)取圆外的一点,那么此时场强就会小于1了,显然这个场是无限延伸的,在无穷远处的场强才等于0。现在想想看,当有多个圆的时候,每个圆都有场,这些场叠加以后,当然某点处的场强就比只有一个圆时的强了,也就是等势线不一样了。所以当两个圆靠近的时候,可以同步地画出等势线


或者比如有两个正电荷,当它们靠近时:

所以现在,假设有n个圆,每个圆的中心是(xi,yi)、半径是ri,那可以写出一个函数

因为一个圆的时候,所有f(x,y)=1的点构成的集合就是这个圆的轮廓、所有f(x,y)>1的点就构成这个圆的内部,那现在同步更新的时候当然就是让f(x,y)≥1即可。那假设你遍历平面上每个像素,然后算出这一点的f(x,y)大于等于1的就保留的话,大概就会得到这样的东西:

可是这样做不仅有一堆锯齿(放大看)、而且用一堆像素预览不得让你不爽?诶,你爽不爽啊,爽的扣1好吗?哎呀,糟了,这里没有弹幕啊,怎么办呢,喂喂,b站什么时候让专栏也有弹幕呢(滑稽)。不好意思,大家见笑了,刚刚是我和我和我与我与我和我的我又疯了。
那现在如果不用像素意味着就要画出轮廓,也就是找到函数f(x,y)=1的点。可是你看啊,这是一个隐式曲面啊,你准备怎么求f(x,y)=1的点?它不像是贝塞尔曲线或贝塞尔曲面那样可以直接采样曲面上的点。你想要画一条贝赛尔曲线就直接用参数方程直接就画出来了。但现在,你不知道有哪些点满足f(x,y)=1怎么办呢,又或者更一般的情况,对于任意的f(x,y)=0,你要怎么绘制?
很好,现在把平面分成网格,并采样每个格点的数值,根据数值"画出"相应的图形,比如有这么一个心形图:

来分析分析怎么样才能得到轮廓。你看啊,当一个方块的四个顶点都是绿色时,这个方块在图形内部,而方块的顶点都是蓝色时,方块在图形外部,所以只要分析方块4个顶点只有部分绿的时候怎么连线即可,对,只有一点绿才能过得去。嗯?又开始了?收敛一点啊!
把下面这个方块单独拿出来分析:

绿点一定在轮廓内、蓝点一定在轮廓外,说明轮廓线穿过了这个方块,所以可以画线:

再比如如果方块是这样的:

同样的方式,可以得到轮廓的近似一部分:

那么方块有4个顶点,所以总共可能出现的情况有2的4次方种,显然同样的分析这16种情况即可

不过这其中的第四列,这两种情况,画线的方式本来是不唯一的,因为你不知道轮廓到底是哪个方向,所以你会发现这两种就算画的线反过来也行

所以如果你为了更加准确,可以多计算一下方块中心是否在图形内部(计算中心点的值),就可以以此为根据来像上图那样画线了。不过呢,其实就算你不管这两种情况也可以,因为这样画线并不算是错的,因为不管怎么画,最后所有的线段都可以连接起来,反正本来就是近似,所以怎么连都不是错的!
这样画轮廓显然使用的方格越多,图形就越精确:

不过,这样画线的话,也需要很多方块才能让轮廓不那么锯齿:

那如果我就想少一点方块,怎么得到更顺滑的轮廓呢?那当然是插值了!直接线性插值,比如有两个数a、b,我怎么求a、b中间的数

当然是(a+b)/2了,那如果要求a到b在0.23处的数字呢,

当然是a+(b-a)*0.23了(这很好理解,因为是从a到b),也可以写成a*(1-0.23)+b*0.23,即a*0.77+b*0.23,那这个也不难理解,首先本身式子变一下样子就得到了,其次,为什么是a乘以0.77,因为a到b的过程才进行到0.23,说明什么,说明a占的份量更重啊!更靠近a,所以是a*0.77和b*0.23,同样的,如果不是进行到0.23而是0.66,那么更靠近b了,所以b占的份量更重,所以是a*0.34和b*0.66,这些应该十分简单,所以如果是a到b在pct处的值就是a+(b-a)*pct或a*(1-pct)+b*pct了。
现在用上线性插值,画线的时候不再是全部用方格边上0.5处的点(不再直接用中点)。

如上图,线段端点用插值得到,比如希望画线的线段端点的数值是1,假设线段端点所在线段的方格顶点的数字是0和1.3,那如果是从0到1.3,那么0*(1-pct)+1.3*pct=1,就可以解出pct了,就知道画在0到1.3的pct处了。线性插值求得的这个位置当然不是精确解(并不是说这个位置的场强就真的是1或者其它你想要的设定值),但是能很大程度地使轮廓更加光滑:

对比之前同样大小方格的,就有非常大的改善

用这么大的方格就已经能得到较为光滑的轮廓了,比起遍历每个像素来硬胡的方法要好多了。这样对于任意的隐式方程,就可以画出等高线了。
很显然,这种方法对于3D也是奏效的,空间体素有8个顶点,所以总共的情况是2^8种,即256种情况


现在想想看,对于平面有16种可能,那你写代码的时候,如果方格每个顶点都用if判断的话,岂不是很不方便?if 第一个顶点在内 and 第二个顶点不在 and 第三个在 and 第四个不在,这样写当然麻烦,考虑到只有在或不在,那就是可以用二进制了。将方格的每个顶点用0、1标记,左下角的顶点是第一个、然后逆时针的方向按顺序记录0或1,如果在内就是1、不在内就是0,第一个顶点的结果排在最低位(即最右边),所以第四个顶点的结果排在最高位。比如当第四个顶点在图形内部、且其它顶点都在外时,就是情况1000了。比如第一、三个顶点在内,第二四不在,那么就是情况0101了。比如第一二顶点在内、第三四不在,那就是情况0011了,这样你写代码岂不是可以直接写比如 if 情况==1011 then 而不用写一堆了。所有情况列举如下

其中有很多对称的情况,所以很多代码是可以直接复制粘贴的,有相当多的重复。
然后还有一些东西需要注意,因为现在是要用aegisub,是那个aegisub啊,没错,就是那个该死的aegisub啊,所以需要绘图的路径是封闭的,也就是需要考虑边界和"零长线段",如果不考虑直接得到一堆线段的话,你就会发现线段当然是不能连出闭合图形的(因为根本没画边界啊):

所以刚刚的16种情况需要再考虑上边界的情况,然后补齐边界:

同时别忘了,"零长线段"也要去掉,比如刚好方格的顶点的数值是你轮廓线上的点的数值,那么在插值以后,得到的线段端点就是重合的,这样的线段是不需要的,为了防止查找相邻线段时出现错误、出现死循环,所以"零长线段"本身就没有用需要去掉。另外,还需要注意轮廓和边界刚好重合的情况!!
然后具体讲一讲得到了一条条线段片段以后,怎么连成绘图代码,以及其它需要注意的东西。将一条条线段放进名叫seg的表里,因为每条路径必然是封闭的、首尾相接的,所以每条路径的起点是什么无所谓,所以直接选seg里的任意一条线段做起点,搜索下一个连上它的线段是什么,当路径的第一点和最后一点相同时,说明这条路径搜索完了,然后当seg表不为空的时候就一直这样搜索一条条路径即可。
但是现在就有一点很重要了,就是搜索得到的路径的方向不是随便怎样都行的,举个例子,如果有绕一圈的圆,那么做融球效果的时候,会得到两条路径,显然是有掏空的,那么这两条路径方向就必须相反:


也就是说,因为该死的assdraw绘图的填充方式只有non-zero规则(我以前讲过的),所以现在连接路径的时候,必须先要考虑路径的方向是否合理。
那么之前我讲过快速的连通域提取,这里也要用类似的思考,同样现在还是用我自己想的算法来解决这个问题。首先,因为路径都是直线段的路径,所以如果求路径的bbox,那必然是图形的紧密包围框,所以如果一条路径的bbox不包含于另一条路径的bbox的话,就可以认为该路径不包含于另一条路径了,反之,如果路径A的bbox包含于路径B的bbox里,并不能说明路径A就包含于路径B里(虽然很多情况下是包含的),所以为了严谨考虑,就需要取A路径上的点,看其是否包含于路径B了,那么知道怎么判断包含关系以后呢?很好,现在有很重要的一点,不知道大家有没有注意到,就是现在不需要提取连通域,而是只需要使每条路径拥有合适的方向而已。你猜咋的,可以直接通过计算路径的"层数"来设定路径方向,比如如果路径是第一层(最外层)的,那假设它的方向是1,那么第二层的路径的方向就设定为相反的-1即可,然后第三层的路径方向又设定为1即可,以此类推。于是,现在的问题变成了怎么知道路径是第几层的。大家回忆一下,我以前讲的高效提取连通域的方法,是不是可以非常快知道谁是最外层?对,没错,非常好,现在,先查看每条路径,如果其的bbox不包含于任意其他路径的bbox的话,说明这条路径必然是最外层,当然如果bbox包含的话,就继续判断路径包含即可,如果路径不包含于任意其它路径的话,当然这条路径就是最外层了!那么,非常美妙的一点就来了,注意哟,来了哟,那就是当我排除(去掉)最外层的路径以后,是不是现在原本第二层的路径就变成最外层了??哇哦,那我只需要每次剥皮(即去掉最外层)的时候,记录下当前层数不就行了吗?正所谓,一层一层的剥开你的皮,所以又到了取名字时间了,我自己想的这个算法就叫做剥皮算法了!!
反复理解一下,去掉第一层、剩下的最外层一定是原本的第二层,去掉原本的第二层、剩下的最外层一定是原本的第三层。因为现在每一条路径都是"有效"的,就是说少了这条路径和多了这条路径得到的绘图必然是不一样的,所以可以直接剥皮。
整理一下思路,当然就可以得到伪代码了:
建立变量layer=0、新路径表new={}
while 路径数量>0时:
layer=layer+1 建立cnt={}为了记录最外层路径的下标,然后后面才能知道去除哪些路径
for倒着遍历路径表
if 第i条路径不包含于其它任意路径 then
第i条路径添加lay标记,下标lay=layer,新路径表加入第i条路径,cnt记录下索引i
endif
endfor
for 遍历cnt :
remove移除掉路径里的第cnt[index]条路径
endfor
end
这样,新的路径表里的每条路径都有了层数标记了。当你一开始建立的变量layer>1时,说明路径不止一层,而如果layer=1,那根本没有必要去检查路径的方向,爱咋咋地对吧。当layer>1时,可以直接设定奇数层路径方向是1、偶数层方向是-1,也就是遍历每一条路径,检查路径的层数、算出路径此时的方向,如果方向不对,就反向该表即可。
好,那么再谈一些细节,比如判断一个矩形是否包含于另一个矩形,这很简单,并不需要判断矩形的4个点是否都包含于另一个矩形里,你只需要判断左上角和右下角的顶点是否包含于另一个矩形即可,如果两个点不都包含于另一个矩形,那自然这个矩形就不可能包含于另一个矩形。
另外一点是,你可能会担心,万一路径有自交呢,你方向设定岂不是会错?很好,非常好,太好了,那就来分析一下,到底需不需要担心路径自交的问题!自交路径大概是这样的

那么如果要连出自交路径,首先网格里会有这样的几个方格

如果要产生路径问题,必须要错开的连接:

比如连了第一个红以后,接下来错开连接到第二个红方格的线段,像是这样错开的连接,那么最后当然会导致路径问题。可是关键是,装线段的seg表的线段是怎么得到的?当然是按照网格一行行或者一列列得到的啊!!那你搜索下一个应该连接的线段时,按seg表顺序搜索,怎么可能率先搜索到错开的一个方块?而是,只可能是这样两种情况:


这两种情况路径连接时并不会出现横跨或交错,当然不会导致路径判断出错了!!
所以现在有了正确方向的所有路径以后,直接连出绘图代码不就行了吗?这样就可以做出融球效果了。那么当然,你也可以考虑其它物体和小球融合,比如你要做水滴从顶部滴落的效果,那么就需要一个长条的矩形和小球相融:

其实这很简单,同样只需要给出该物体正确的渐变即可:

越靠近顶部密度越大、越远离顶部则密度越小。假设顶部的y坐标是ty、长条厚度是d,那么可以认为任意一点(x,y)的场强是d/math.abs(ty-y),这样不就有了针对顶部长条的合理的场了吗?并且融合的程度你想想也可以调节的,比如你套个次方,那么有了幂以后,当某点到顶部的距离超过长条的厚度以后,场强就可以变得非常小了不是吗,比如用(d/math.abs(ty-y))^2
同样的,比如你还可以设定物体是方块,比如:

怎么样,很简单吧?同样也是越靠近方块中心密度越大(就是图中现在的越黑)

所以就像一开始说的,做融合效果可以先高斯模糊再调节对比度。不过当然,高斯模糊会使得物体边缘没有那么尖锐、边缘会变得平滑,这便是用高斯模糊的缺点。(所以正确方法不是用高斯模糊来做)
好了,讲了这么多,你应该就知道了,我为什么没有选择用贝塞尔曲线来做融球效果了。因为虽然用贝塞尔曲线可以连上两个球:

但是,用曲线连接有一堆缺陷:效果不够准确、两圆融合以后图形面积不合理、连接只能圆和圆俩俩连接(而融合应是多个物体间的融合)、专门连接两圆那么想要连接其它物体(如五角星)呢?所以,一般情况下,我当然是并不建议使用贝塞尔曲线做所谓的融球效果的。
并且用曲线连接还有一个缺陷是只能做做融合,如果要做挤压效果怎么办呢?举个例子,如果有一个正电荷一个负电荷的话:


显然,现在的等势线就和全是正电荷不一样了,那么明显可以利用这个来做互相挤压的效果。那现在就相当于每个小球可以带正电可以带负电,那么某点的场强就是根据电性来加或者减即可。那结果当然可能有正有负了,那么你画线的时候可以正负都画出来,比如-1和1的都一块画了:

也就是取绝对值。不过这样如果你觉得不清楚,那你可以分开画嘛,把所有带正电的画出来、再把所有带负电的画出来,然后可以给不同电性的球不同的边框颜色,这样看起来就可以分得更清楚:

可以看到,只要你理解了这个东西,你不仅可以做融合效果还可以做挤压效果,不仅可以是小球之间,还可以是其他物体之间的。
最后提醒一点,绘图代码尽量不要取整,那样图形看起来会锯齿化,我以前说过我强迫症,一般都是保留两位小数的,大家可以自己试试看区别在哪。
那么溶球的简单介绍就这样了。融球可以用来做异常多、异常多、异常多的效果!
还是照例,相关的代码就在视频里讲了
(本文写于2022年1月25日,应该会在几个月之内发布吧)