【Aegisub】融球效果、粘性小球與液體水滴效果

? ? ? ? 這次又來(lái)講一個(gè)沒(méi)人用Aegisub做過(guò)的東西。下面這幾個(gè)動(dòng)圖都是用aegisub做出來(lái)的,而且都是一個(gè)個(gè)大的繪圖、并不是用像素堆出來(lái)的





是的沒(méi)錯(cuò),利用液滴連火焰效果都可以做,生成一張張隨機(jī)的火焰圖,并不是用一堆像素粒子堆的,所以預(yù)覽起來(lái)特別爽啊!啊,我好爽啊,我不行了,太爽了,怎么這么爽??!
????????那么這個(gè)要怎么做呢,先看下面這個(gè)動(dòng)圖

可以看到,兩個(gè)本身沒(méi)有融合效果的圓在進(jìn)行模糊以后,就有了融合的感覺(jué)了,所以在此基礎(chǔ)上調(diào)一下對(duì)比度就可以得到真正的溶球效果了。所以你可以自己試試,在aegisub里用高斯模糊,然后你拖動(dòng)字幕定位點(diǎn),就可以看到兩個(gè)球有融合的感覺(jué),不過(guò)當(dāng)然調(diào)對(duì)比度就沒(méi)辦法了,所以你可能就會(huì)想那這個(gè)不就需要用像素來(lái)做了?不過(guò),不要小瞧愛(ài)的力量??!相信愛(ài)?。?!
? ? ? ? 現(xiàn)在假設(shè)每個(gè)物體對(duì)象都有一個(gè)場(chǎng),并用一個(gè)場(chǎng)函數(shù)表示這個(gè)場(chǎng)。比方一個(gè)圓,假設(shè)一點(diǎn)距離場(chǎng)心越近場(chǎng)強(qiáng)就越強(qiáng),那這個(gè)場(chǎng)看起來(lái)就可以是這樣的:

或者你可以想象成離中心越近的地方就密度越大、所以顏色越深。好了,總之離場(chǎng)心越近場(chǎng)強(qiáng)就越強(qiáng)很簡(jiǎn)單,圓的方程是

其中(x0,y0)是圓的中心,(x,y)是圓上的點(diǎn),r是半徑,那么這個(gè)

大于等于1的時(shí)候,(x,y)當(dāng)然就可以是圓上和圓內(nèi)的任意一點(diǎn)了,并且(x,y)距離中心越近的時(shí)候,場(chǎng)強(qiáng)就越大,在場(chǎng)心的時(shí)候此時(shí)的場(chǎng)強(qiáng)就無(wú)窮大。而當(dāng)然,如果你(x,y)取圓外的一點(diǎn),那么此時(shí)場(chǎng)強(qiáng)就會(huì)小于1了,顯然這個(gè)場(chǎng)是無(wú)限延伸的,在無(wú)窮遠(yuǎn)處的場(chǎng)強(qiáng)才等于0。現(xiàn)在想想看,當(dāng)有多個(gè)圓的時(shí)候,每個(gè)圓都有場(chǎng),這些場(chǎng)疊加以后,當(dāng)然某點(diǎn)處的場(chǎng)強(qiáng)就比只有一個(gè)圓時(shí)的強(qiáng)了,也就是等勢(shì)線不一樣了。所以當(dāng)兩個(gè)圓靠近的時(shí)候,可以同步地畫(huà)出等勢(shì)線


或者比如有兩個(gè)正電荷,當(dāng)它們靠近時(shí):

所以現(xiàn)在,假設(shè)有n個(gè)圓,每個(gè)圓的中心是(xi,yi)、半徑是ri,那可以寫(xiě)出一個(gè)函數(shù)

因?yàn)橐粋€(gè)圓的時(shí)候,所有f(x,y)=1的點(diǎn)構(gòu)成的集合就是這個(gè)圓的輪廓、所有f(x,y)>1的點(diǎn)就構(gòu)成這個(gè)圓的內(nèi)部,那現(xiàn)在同步更新的時(shí)候當(dāng)然就是讓f(x,y)≥1即可。那假設(shè)你遍歷平面上每個(gè)像素,然后算出這一點(diǎn)的f(x,y)大于等于1的就保留的話,大概就會(huì)得到這樣的東西:

可是這樣做不僅有一堆鋸齒(放大看)、而且用一堆像素預(yù)覽不得讓你不爽?誒,你爽不爽啊,爽的扣1好嗎?哎呀,糟了,這里沒(méi)有彈幕啊,怎么辦呢,喂喂,b站什么時(shí)候讓專(zhuān)欄也有彈幕呢(滑稽)。不好意思,大家見(jiàn)笑了,剛剛是我和我和我與我與我和我的我又瘋了。
? ? ? ? 那現(xiàn)在如果不用像素意味著就要畫(huà)出輪廓,也就是找到函數(shù)f(x,y)=1的點(diǎn)??墒悄憧窗?,這是一個(gè)隱式曲面啊,你準(zhǔn)備怎么求f(x,y)=1的點(diǎn)?它不像是貝塞爾曲線或貝塞爾曲面那樣可以直接采樣曲面上的點(diǎn)。你想要畫(huà)一條貝賽爾曲線就直接用參數(shù)方程直接就畫(huà)出來(lái)了。但現(xiàn)在,你不知道有哪些點(diǎn)滿(mǎn)足f(x,y)=1怎么辦呢,又或者更一般的情況,對(duì)于任意的f(x,y)=0,你要怎么繪制?
? ? ? ? 很好,現(xiàn)在把平面分成網(wǎng)格,并采樣每個(gè)格點(diǎn)的數(shù)值,根據(jù)數(shù)值"畫(huà)出"相應(yīng)的圖形,比如有這么一個(gè)心形圖:

來(lái)分析分析怎么樣才能得到輪廓。你看啊,當(dāng)一個(gè)方塊的四個(gè)頂點(diǎn)都是綠色時(shí),這個(gè)方塊在圖形內(nèi)部,而方塊的頂點(diǎn)都是藍(lán)色時(shí),方塊在圖形外部,所以只要分析方塊4個(gè)頂點(diǎn)只有部分綠的時(shí)候怎么連線即可,對(duì),只有一點(diǎn)綠才能過(guò)得去。嗯?又開(kāi)始了?收斂一點(diǎn)??!
? ? ? ??把下面這個(gè)方塊單獨(dú)拿出來(lái)分析:

綠點(diǎn)一定在輪廓內(nèi)、藍(lán)點(diǎn)一定在輪廓外,說(shuō)明輪廓線穿過(guò)了這個(gè)方塊,所以可以畫(huà)線:

再比如如果方塊是這樣的:

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

那么方塊有4個(gè)頂點(diǎn),所以總共可能出現(xiàn)的情況有2的4次方種,顯然同樣的分析這16種情況即可

不過(guò)這其中的第四列,這兩種情況,畫(huà)線的方式本來(lái)是不唯一的,因?yàn)槟悴恢垒喞降资悄膫€(gè)方向,所以你會(huì)發(fā)現(xiàn)這兩種就算畫(huà)的線反過(guò)來(lái)也行

所以如果你為了更加準(zhǔn)確,可以多計(jì)算一下方塊中心是否在圖形內(nèi)部(計(jì)算中心點(diǎn)的值),就可以以此為根據(jù)來(lái)像上圖那樣畫(huà)線了。不過(guò)呢,其實(shí)就算你不管這兩種情況也可以,因?yàn)檫@樣畫(huà)線并不算是錯(cuò)的,因?yàn)椴还茉趺串?huà),最后所有的線段都可以連接起來(lái),反正本來(lái)就是近似,所以怎么連都不是錯(cuò)的!
? ? ? ? 這樣畫(huà)輪廓顯然使用的方格越多,圖形就越精確:

不過(guò),這樣畫(huà)線的話,也需要很多方塊才能讓輪廓不那么鋸齒:

那如果我就想少一點(diǎn)方塊,怎么得到更順滑的輪廓呢?那當(dāng)然是插值了!直接線性插值,比如有兩個(gè)數(shù)a、b,我怎么求a、b中間的數(shù)

當(dāng)然是(a+b)/2了,那如果要求a到b在0.23處的數(shù)字呢,

當(dāng)然是a+(b-a)*0.23了(這很好理解,因?yàn)槭菑腶到b),也可以寫(xiě)成a*(1-0.23)+b*0.23,即a*0.77+b*0.23,那這個(gè)也不難理解,首先本身式子變一下樣子就得到了,其次,為什么是a乘以0.77,因?yàn)閍到b的過(guò)程才進(jìn)行到0.23,說(shuō)明什么,說(shuō)明a占的份量更重?。「拷黙,所以是a*0.77和b*0.23,同樣的,如果不是進(jìn)行到0.23而是0.66,那么更靠近b了,所以b占的份量更重,所以是a*0.34和b*0.66,這些應(yīng)該十分簡(jiǎn)單,所以如果是a到b在pct處的值就是a+(b-a)*pct或a*(1-pct)+b*pct了。
? ? ? ? 現(xiàn)在用上線性插值,畫(huà)線的時(shí)候不再是全部用方格邊上0.5處的點(diǎn)(不再直接用中點(diǎn))。

如上圖,線段端點(diǎn)用插值得到,比如希望畫(huà)線的線段端點(diǎn)的數(shù)值是1,假設(shè)線段端點(diǎn)所在線段的方格頂點(diǎn)的數(shù)字是0和1.3,那如果是從0到1.3,那么0*(1-pct)+1.3*pct=1,就可以解出pct了,就知道畫(huà)在0到1.3的pct處了。線性插值求得的這個(gè)位置當(dāng)然不是精確解(并不是說(shuō)這個(gè)位置的場(chǎng)強(qiáng)就真的是1或者其它你想要的設(shè)定值),但是能很大程度地使輪廓更加光滑:

對(duì)比之前同樣大小方格的,就有非常大的改善

用這么大的方格就已經(jīng)能得到較為光滑的輪廓了,比起遍歷每個(gè)像素來(lái)硬胡的方法要好多了。這樣對(duì)于任意的隱式方程,就可以畫(huà)出等高線了。
? ? ? ? 很顯然,這種方法對(duì)于3D也是奏效的,空間體素有8個(gè)頂點(diǎn),所以總共的情況是2^8種,即256種情況


? ? ? ? 現(xiàn)在想想看,對(duì)于平面有16種可能,那你寫(xiě)代碼的時(shí)候,如果方格每個(gè)頂點(diǎn)都用if判斷的話,豈不是很不方便?if 第一個(gè)頂點(diǎn)在內(nèi) and 第二個(gè)頂點(diǎn)不在 and 第三個(gè)在 and 第四個(gè)不在,這樣寫(xiě)當(dāng)然麻煩,考慮到只有在或不在,那就是可以用二進(jìn)制了。將方格的每個(gè)頂點(diǎn)用0、1標(biāo)記,左下角的頂點(diǎn)是第一個(gè)、然后逆時(shí)針的方向按順序記錄0或1,如果在內(nèi)就是1、不在內(nèi)就是0,第一個(gè)頂點(diǎn)的結(jié)果排在最低位(即最右邊),所以第四個(gè)頂點(diǎn)的結(jié)果排在最高位。比如當(dāng)?shù)谒膫€(gè)頂點(diǎn)在圖形內(nèi)部、且其它頂點(diǎn)都在外時(shí),就是情況1000了。比如第一、三個(gè)頂點(diǎn)在內(nèi),第二四不在,那么就是情況0101了。比如第一二頂點(diǎn)在內(nèi)、第三四不在,那就是情況0011了,這樣你寫(xiě)代碼豈不是可以直接寫(xiě)比如 if 情況==1011 then 而不用寫(xiě)一堆了。所有情況列舉如下

其中有很多對(duì)稱(chēng)的情況,所以很多代碼是可以直接復(fù)制粘貼的,有相當(dāng)多的重復(fù)。
? ? ? ? 然后還有一些東西需要注意,因?yàn)楝F(xiàn)在是要用aegisub,是那個(gè)aegisub啊,沒(méi)錯(cuò),就是那個(gè)該死的aegisub啊,所以需要繪圖的路徑是封閉的,也就是需要考慮邊界和"零長(zhǎng)線段",如果不考慮直接得到一堆線段的話,你就會(huì)發(fā)現(xiàn)線段當(dāng)然是不能連出閉合圖形的(因?yàn)楦緵](méi)畫(huà)邊界啊):

所以剛剛的16種情況需要再考慮上邊界的情況,然后補(bǔ)齊邊界:

同時(shí)別忘了,"零長(zhǎng)線段"也要去掉,比如剛好方格的頂點(diǎn)的數(shù)值是你輪廓線上的點(diǎn)的數(shù)值,那么在插值以后,得到的線段端點(diǎn)就是重合的,這樣的線段是不需要的,為了防止查找相鄰線段時(shí)出現(xiàn)錯(cuò)誤、出現(xiàn)死循環(huán),所以"零長(zhǎng)線段"本身就沒(méi)有用需要去掉。另外,還需要注意輪廓和邊界剛好重合的情況??!
? ? ? ? 然后具體講一講得到了一條條線段片段以后,怎么連成繪圖代碼,以及其它需要注意的東西。將一條條線段放進(jìn)名叫seg的表里,因?yàn)?span id="s0sssss00s" class="color-green-03">每條路徑必然是封閉的、首尾相接的,所以每條路徑的起點(diǎn)是什么無(wú)所謂,所以直接選seg里的任意一條線段做起點(diǎn),搜索下一個(gè)連上它的線段是什么,當(dāng)路徑的第一點(diǎn)和最后一點(diǎn)相同時(shí),說(shuō)明這條路徑搜索完了,然后當(dāng)seg表不為空的時(shí)候就一直這樣搜索一條條路徑即可。
? ? ? ? 但是現(xiàn)在就有一點(diǎn)很重要了,就是搜索得到的路徑的方向不是隨便怎樣都行的,舉個(gè)例子,如果有繞一圈的圓,那么做融球效果的時(shí)候,會(huì)得到兩條路徑,顯然是有掏空的,那么這兩條路徑方向就必須相反:


也就是說(shuō),因?yàn)樵撍赖腶ssdraw繪圖的填充方式只有non-zero規(guī)則(我以前講過(guò)的),所以現(xiàn)在連接路徑的時(shí)候,必須先要考慮路徑的方向是否合理。
? ? ? ? 那么之前我講過(guò)快速的連通域提取,這里也要用類(lèi)似的思考,同樣現(xiàn)在還是用我自己想的算法來(lái)解決這個(gè)問(wèn)題。首先,因?yàn)槁窂蕉际侵本€段的路徑,所以如果求路徑的bbox,那必然是圖形的緊密包圍框,所以如果一條路徑的bbox不包含于另一條路徑的bbox的話,就可以認(rèn)為該路徑不包含于另一條路徑了,反之,如果路徑A的bbox包含于路徑B的bbox里,并不能說(shuō)明路徑A就包含于路徑B里(雖然很多情況下是包含的),所以為了嚴(yán)謹(jǐn)考慮,就需要取A路徑上的點(diǎn),看其是否包含于路徑B了,那么知道怎么判斷包含關(guān)系以后呢?很好,現(xiàn)在有很重要的一點(diǎn),不知道大家有沒(méi)有注意到,就是現(xiàn)在不需要提取連通域,而是只需要使每條路徑擁有合適的方向而已。你猜咋的,可以直接通過(guò)計(jì)算路徑的"層數(shù)"來(lái)設(shè)定路徑方向,比如如果路徑是第一層(最外層)的,那假設(shè)它的方向是1,那么第二層的路徑的方向就設(shè)定為相反的-1即可,然后第三層的路徑方向又設(shè)定為1即可,以此類(lèi)推。于是,現(xiàn)在的問(wèn)題變成了怎么知道路徑是第幾層的。大家回憶一下,我以前講的高效提取連通域的方法,是不是可以非??熘勒l(shuí)是最外層?對(duì),沒(méi)錯(cuò),非常好,現(xiàn)在,先查看每條路徑,如果其的bbox不包含于任意其他路徑的bbox的話,說(shuō)明這條路徑必然是最外層,當(dāng)然如果bbox包含的話,就繼續(xù)判斷路徑包含即可,如果路徑不包含于任意其它路徑的話,當(dāng)然這條路徑就是最外層了!那么,非常美妙的一點(diǎn)就來(lái)了,注意喲,來(lái)了喲,那就是當(dāng)我排除(去掉)最外層的路徑以后,是不是現(xiàn)在原本第二層的路徑就變成最外層了??哇哦,那我只需要每次剝皮(即去掉最外層)的時(shí)候,記錄下當(dāng)前層數(shù)不就行了嗎?正所謂,一層一層的剝開(kāi)你的皮,所以又到了取名字時(shí)間了,我自己想的這個(gè)算法就叫做剝皮算法了!!
????? ? 反復(fù)理解一下,去掉第一層、剩下的最外層一定是原本的第二層,去掉原本的第二層、剩下的最外層一定是原本的第三層。因?yàn)楝F(xiàn)在每一條路徑都是"有效"的,就是說(shuō)少了這條路徑和多了這條路徑得到的繪圖必然是不一樣的,所以可以直接剝皮。
? ? ? ? 整理一下思路,當(dāng)然就可以得到偽代碼了:
建立變量layer=0、新路徑表new={}
while 路徑數(shù)量>0時(shí):
? ? layer=layer+1 建立cnt={}為了記錄最外層路徑的下標(biāo),然后后面才能知道去除哪些路徑
? ? for倒著遍歷路徑表
? ? ? ? if 第i條路徑不包含于其它任意路徑 then
? ? ? ? ? ? 第i條路徑添加lay標(biāo)記,下標(biāo)lay=layer,新路徑表加入第i條路徑,cnt記錄下索引i
? ? ? ? endif
? ? endfor
? ? for 遍歷cnt :
? ? ? ? remove移除掉路徑里的第cnt[index]條路徑
? ? endfor
end
? ? ? ? 這樣,新的路徑表里的每條路徑都有了層數(shù)標(biāo)記了。當(dāng)你一開(kāi)始建立的變量layer>1時(shí),說(shuō)明路徑不止一層,而如果layer=1,那根本沒(méi)有必要去檢查路徑的方向,愛(ài)咋咋地對(duì)吧。當(dāng)layer>1時(shí),可以直接設(shè)定奇數(shù)層路徑方向是1、偶數(shù)層方向是-1,也就是遍歷每一條路徑,檢查路徑的層數(shù)、算出路徑此時(shí)的方向,如果方向不對(duì),就反向該表即可。
? ? ? ? 好,那么再談一些細(xì)節(jié),比如判斷一個(gè)矩形是否包含于另一個(gè)矩形,這很簡(jiǎn)單,并不需要判斷矩形的4個(gè)點(diǎn)是否都包含于另一個(gè)矩形里,你只需要判斷左上角和右下角的頂點(diǎn)是否包含于另一個(gè)矩形即可,如果兩個(gè)點(diǎn)不都包含于另一個(gè)矩形,那自然這個(gè)矩形就不可能包含于另一個(gè)矩形。
? ? ? ? 另外一點(diǎn)是,你可能會(huì)擔(dān)心,萬(wàn)一路徑有自交呢,你方向設(shè)定豈不是會(huì)錯(cuò)?很好,非常好,太好了,那就來(lái)分析一下,到底需不需要擔(dān)心路徑自交的問(wèn)題!自交路徑大概是這樣的

那么如果要連出自交路徑,首先網(wǎng)格里會(huì)有這樣的幾個(gè)方格

如果要產(chǎn)生路徑問(wèn)題,必須要錯(cuò)開(kāi)的連接:

比如連了第一個(gè)紅以后,接下來(lái)錯(cuò)開(kāi)連接到第二個(gè)紅方格的線段,像是這樣錯(cuò)開(kāi)的連接,那么最后當(dāng)然會(huì)導(dǎo)致路徑問(wèn)題。可是關(guān)鍵是,裝線段的seg表的線段是怎么得到的?當(dāng)然是按照網(wǎng)格一行行或者一列列得到的啊??!那你搜索下一個(gè)應(yīng)該連接的線段時(shí),按seg表順序搜索,怎么可能率先搜索到錯(cuò)開(kāi)的一個(gè)方塊?而是,只可能是這樣兩種情況:


這兩種情況路徑連接時(shí)并不會(huì)出現(xiàn)橫跨或交錯(cuò),當(dāng)然不會(huì)導(dǎo)致路徑判斷出錯(cuò)了??!
? ? ? ? 所以現(xiàn)在有了正確方向的所有路徑以后,直接連出繪圖代碼不就行了嗎?這樣就可以做出融球效果了。那么當(dāng)然,你也可以考慮其它物體和小球融合,比如你要做水滴從頂部滴落的效果,那么就需要一個(gè)長(zhǎng)條的矩形和小球相融:

其實(shí)這很簡(jiǎn)單,同樣只需要給出該物體正確的漸變即可:

越靠近頂部密度越大、越遠(yuǎn)離頂部則密度越小。假設(shè)頂部的y坐標(biāo)是ty、長(zhǎng)條厚度是d,那么可以認(rèn)為任意一點(diǎn)(x,y)的場(chǎng)強(qiáng)是d/math.abs(ty-y),這樣不就有了針對(duì)頂部長(zhǎng)條的合理的場(chǎng)了嗎?并且融合的程度你想想也可以調(diào)節(jié)的,比如你套個(gè)次方,那么有了冪以后,當(dāng)某點(diǎn)到頂部的距離超過(guò)長(zhǎng)條的厚度以后,場(chǎng)強(qiáng)就可以變得非常小了不是嗎,比如用(d/math.abs(ty-y))^2
? ? ? ? 同樣的,比如你還可以設(shè)定物體是方塊,比如:

怎么樣,很簡(jiǎn)單吧?同樣也是越靠近方塊中心密度越大(就是圖中現(xiàn)在的越黑)

所以就像一開(kāi)始說(shuō)的,做融合效果可以先高斯模糊再調(diào)節(jié)對(duì)比度。不過(guò)當(dāng)然,高斯模糊會(huì)使得物體邊緣沒(méi)有那么尖銳、邊緣會(huì)變得平滑,這便是用高斯模糊的缺點(diǎn)。(所以正確方法不是用高斯模糊來(lái)做)
? ? ? ? 好了,講了這么多,你應(yīng)該就知道了,我為什么沒(méi)有選擇用貝塞爾曲線來(lái)做融球效果了。因?yàn)殡m然用貝塞爾曲線可以連上兩個(gè)球:

但是,用曲線連接有一堆缺陷:效果不夠準(zhǔn)確、兩圓融合以后圖形面積不合理、連接只能圓和圓倆倆連接(而融合應(yīng)是多個(gè)物體間的融合)、專(zhuān)門(mén)連接兩圓那么想要連接其它物體(如五角星)呢?所以,一般情況下,我當(dāng)然是并不建議使用貝塞爾曲線做所謂的融球效果的。
? ? ? ? 并且用曲線連接還有一個(gè)缺陷是只能做做融合,如果要做擠壓效果怎么辦呢?舉個(gè)例子,如果有一個(gè)正電荷一個(gè)負(fù)電荷的話:


顯然,現(xiàn)在的等勢(shì)線就和全是正電荷不一樣了,那么明顯可以利用這個(gè)來(lái)做互相擠壓的效果。那現(xiàn)在就相當(dāng)于每個(gè)小球可以帶正電可以帶負(fù)電,那么某點(diǎn)的場(chǎng)強(qiáng)就是根據(jù)電性來(lái)加或者減即可。那結(jié)果當(dāng)然可能有正有負(fù)了,那么你畫(huà)線的時(shí)候可以正負(fù)都畫(huà)出來(lái),比如-1和1的都一塊畫(huà)了:

也就是取絕對(duì)值。不過(guò)這樣如果你覺(jué)得不清楚,那你可以分開(kāi)畫(huà)嘛,把所有帶正電的畫(huà)出來(lái)、再把所有帶負(fù)電的畫(huà)出來(lái),然后可以給不同電性的球不同的邊框顏色,這樣看起來(lái)就可以分得更清楚:

可以看到,只要你理解了這個(gè)東西,你不僅可以做融合效果還可以做擠壓效果,不僅可以是小球之間,還可以是其他物體之間的。
? ? ? ? 最后提醒一點(diǎn),繪圖代碼盡量不要取整,那樣圖形看起來(lái)會(huì)鋸齒化,我以前說(shuō)過(guò)我強(qiáng)迫癥,一般都是保留兩位小數(shù)的,大家可以自己試試看區(qū)別在哪。
????????那么溶球的簡(jiǎn)單介紹就這樣了。融球可以用來(lái)做異常多、異常多、異常多的效果!
? ? ? ??還是照例,相關(guān)的代碼就在視頻里講了
(本文寫(xiě)于2022年1月25日,應(yīng)該會(huì)在幾個(gè)月之內(nèi)發(fā)布吧)