本篇文章将借两个例子对实际情景下字幕特效的制作进行介绍。

案例一・字幕组署名

大多数字幕组会在字幕中进行署名,一般是记录制作字幕的staff。
并且其中大多数是定式的顶部字幕,有少数字幕组会将字幕staff融入动画的标题动画或者动画staff中。
此外也有极少会用一些特效进行展示。

使用特效进行展示会有诸多不同,需要划分出一片区域并进行图案及文字的设计。
其中较为简单的是字幕组的logo配上文字,本质上只是设定位置+绘图+特定对齐(一般是靠左对齐)文字。
如果要制作比较复杂的效果,为了更好地展示文字就需要一个底色背景,也就最好是用一个“容器”来框住文字。

下图为“喫茶ステラと時計の喋”的印象图:

原本我是无意中看到了《喫茶ステラと死神の蝶》的logo,发现比较适合作署名的特效,随后发现“喫茶ステラと時計の喋”的图。
对于这张图,可以将「喫茶ステラ」替换为组名,并在下方「時計の喋」区域写入staff,可以想象到会有一个不错的效果。

一种简单有效的实现方式是,对图像进行一些细节上的修改,并通过一些工具将图像直接转换成ASS格式。
但这样的处理并不是很“美”,所以本文介绍一个我自己常用的笨方法。

使用ASSDraw3对主要元素进行简单复原。
简单的曲线均使用一个贝塞尔曲线即可,没必要太精确,可以稍微扩大一点元素;
在画的时候一定要分部画,便于对不同的素材使用不同的效果;
对于镜像或者旋转,可以直接在Aegisub里写个简单的代码运行获得,不过我个人是直接python脚本单独处理;
对于圆形,如果较小的素材就直接两个贝塞尔曲线,如果是会显示一定大小的素材则使用四个贝塞尔曲线(贝塞尔画不出完美的圆,两条曲线拼凑出来的橄榄球形在足够大时会穿帮)。

在勾勒基础素材之后:

最顶部和最底部的“边框”可以单独作为一行;
两边的翅膀因为与边框颜色不同,可以另作一行(当然也可以硬塞进一行);
下半部分剩余的素材可以作一行,因为这些素材基本不需要额外调整;
上半部分剩余的素材大概率需要进行修改并另作一行,因为如果在「喫茶ステラ」的位置写组名那么因为文字不同大概率会需要调整素材的位置。

对这些素材,在\an7\p1的基础上设定同一个\pos及对应的\1c后:

这里会遇到第一个问题:如何填充白色底色?
一个最直接的思路就是为每一行都设置\3c&HFFFFFF&\bord20,这样每个元素都会有边框自然就会有底色。
但很明显这个答案是错误的:

不同行的边框会互相覆盖,并且这里并不能通过设置层级的方式来避免覆盖。
也就是需要额外有一行低层级的边框作为底色,这个边框需要有所有元素:

再加一个大小适宜的矩形作底色后:

到这里就有个模样了,剩余的部分大致就是位于中上部分的组名、位于中下部分的字幕staff、位于底部的小字、顶部的时钟。

对于原图的「喫茶ステラ」字样很难找到一个足够近似的字体,并且原图中字样是由不同色彩的笔画所组成的。
不过这里并不是要做屏字还原,并不需要考虑「还原度」,更关键的是「契合度」,只要与其它元素搭配起来效果好就行。
所以我最后是使用了一个比较圆润可爱的字体,因为不同色彩笔画实现较为麻烦所以也只使用了单一色彩:

中间的「の」则是因为空隙过大(原图五个字宽但我们组名只有四个字)所硬塞的。

字幕staff区域则是看自己喜好了,而位于底部的小字在原图中为「CAT & STRAWBERRY & PANCAKE」,这里就随便写些自己想写的就行:

最后就是顶部的时钟了,这里再看一下原图:

首先是时钟本身有过渡,有一小半的区域是不可见的,这个效果要简单实现只有一种方式:对于一个完整时钟,在下方放一个白色矩形挡住一部分,为白色矩形添加高斯模糊。
时钟的圆最好是用四条贝塞尔曲线组成,因为该圆有一定大小如果还是只用两条贝塞尔会穿帮(但也不需要用更高条数,四条已经足够像圆了)。
原图的时钟边框其实是两个圆环,如果要硬生生用绘图还原圆环的话是比较麻烦的,我们可以直接使用\bord添加边框来模拟圆环,也就是需要画两个不同大小的圆,此时可以简化为对于相同的圆使用不同的\fscx\fscy进行缩放。
对于数字,一个直接的处理方式是计算对应坐标后直接\pos,另外就是设定一个刻度的坐标,然后使用\org将旋转中心设为时钟中心再用\frz进行旋转,但这时数字不会全是原图那样竖着的,可以强行再用\fax\fay掰正(不过我觉得歪着也无所谓所以就没再修改)。
对于时针,既然都有时针了也可以顺便做个时针转动的效果,我最后是做的两段变速旋转的效果,因为下半区域不可见所以可以在转到下半时换一个速度系数让速度变化更加舒适。

最后说一些不太会注意到的地方,「極彩花夢」四个字是用了\be的,稍微给点柔化模糊会让文字更契合(或者说,在不给模糊的情况下字会有些违和,给人明显感觉是另一个“图层”的东西,而在给高斯模糊的情况下字会非常难看)。
对于这个例子,对于不同的元素需要设置不同的层级以避免错误的覆盖,并且还需要预留底色的层级。
没什么技术含量,主要是得有每个细节该怎么处理的思路。

案例二・标题动画

要还原这个效果是比较麻烦的。
每个字从左到右出现的这个效果相信就能难住不少人;
最会让人在意的是「恋」字的运动,变速、曲线、中转,都该如何实现;
在「恋」字撞击红心时,红心的形变、变速随机散开的小红心;
在「恋」字运动时有撞击另外两个字,该如何模拟。
但对于一些简单的部分就不多作阐述了,例如底色和边框。

当初制作这个效果时,为了便于调整我是一开始就给了几行单独配置一些信息,比如三行字分别用的字体、使用的翻译/彩蛋英文、坐标、字体大小等。

Comment: 0,0:13:09.06,0:13:09.06,Ex-effects,标题配置,0,0,0,code line,title = "这份恋情望你察觉" title_2 = "Eienno la vidato" title_4 = "BY KYOKUSAIYUME" title_x, title_y = 1436, 820 title_x_1, title_y_1 = 1818, 1028 title_w, title_h = title_x_1 - title_x, title_y_1 - title_y title_x, title_y = 1920 - title_x_1, 1080 - title_y_1 if (fxgroup.title_main == true) then title_x, title_y = 1436, 820 end title_line_w = 1790 - 1464 title_line_1_y, title_line_2_y, title_line_3_y, title_line_4_y = 906, 918, 942, 950 title_line_1_y, title_line_2_y, title_line_3_y, title_line_4_y = title_line_1_y - (1080 - title_y - title_h) + title_y, title_line_2_y - (1080 - title_y - title_h) + title_y, title_line_3_y - (1080 - title_y - title_h) + title_y, title_line_4_y - (1080 - title_y - title_h) + title_y if (fxgroup.title_main == true) then title_line_1_y, title_line_2_y, title_line_3_y, title_line_4_y = 906, 918, 942, 950 end
Comment: 0,0:13:09.06,0:13:09.06,Ex-effects,样式配置,0,0,0,code line,title_line_1_styleref, title_line_2_styleref, title_line_4_styleref = line.styleref, line.styleref, line.styleref title_line_1_styleref.fontname = "方正兰亭圆_GBK" title_line_1_styleref.fontsize = 10 title_line_2_styleref.fontname = "Josefin Sans SemiBold" title_line_2_styleref.fontsize = 10 title_line_2_swidth = {} title_line_2_twidth = 0 title_line_2_fs = 30 / strinfo(title_2, title_line_2_styleref)["height"] * 10 title_line_4_styleref.fontname = "方正兰亭圆_GBK_中" title_line_4_styleref.fontsize = 10 title_line_4_swidth = {} title_line_4_twidth = 0 title_line_4_fs = 25 / strinfo(title_4, title_line_4_styleref)["height"] * 10 for i = 1, strlen(title_2) do title_line_2_swidth[i] = strinfo(strsub(title_2, i, 1, title_line_2_styleref))["width"] / 10 * title_line_2_fs title_line_2_twidth = title_line_2_twidth + title_line_2_swidth[i] end function title_line_2_sum(j) local sum = 0 for i = 1, j do sum = sum + title_line_2_swidth[i] end return sum end title_line_3_space = 10 title_line_3_swidth = { title_line_w - 0 - title_line_3_space * 5 } for i = 2, 6 do title_line_3_swidth[i] = math.random(0, title_line_3_swidth[1]) end table.sort(title_line_3_swidth) for i = #title_line_3_swidth, 1, -1 do title_line_3_swidth[i] = title_line_3_swidth[i] - (i == 1 and 0 or title_line_3_swidth[i - 1]) end title_line_3_swidth = shuffle(title_line_3_swidth) function title_line_3_sum(j) local sum = 0 for i = 1, j do sum = sum + title_line_3_swidth[i] end return sum end for i = 1, strlen(title_4) do title_line_4_swidth[i] = strinfo(strsub(title_4, i, 1, title_line_4_styleref))["width"] / 10 * title_line_4_fs title_line_4_twidth = title_line_4_twidth + title_line_4_swidth[i] end function title_line_4_sum(j) local sum = 0 for i = 1, j do sum = sum + title_line_4_swidth[i] end return sum end

所以一开始就是奔着一个非常复杂的目标去的。

对于第一行中的普通字,首先要做的就是!relayer(1)!来保证层级和是!retime!调整时间,然后为了能够让一个template line能够生成每一个字还需要使用!maxloop!进行循环生成。
基础的标签要用到\an\fn\fs\1c,再通过一些计算来设置\pos,这时为了文字足够柔和我使用了\be
那么问题来了,从左到右逐渐出现该如何实现呢?没错,就是使用\clip配合\t实现动画效果,类似\clip(x1,y1,x1,y2)\t(0,500,1,\clip(x1,y1,x2,y2))就能实现从左到右逐渐出现,但这样不可避免地会非常长,因为需要为每个字分别计算坐标。
其实还有一个标签能够实现,那就是\k的派生标签\K,这个标签很少会用到存在感很低,但只要用简短的\K30就能实现从左到右逐渐出现。
这一template行完整内容为(不用在意其中一些奇怪的函数名,参考大致内容即可):

Comment: 0,0:13:09.23,0:13:09.23,Ex-effects,普通字,0,0,0,template line notext fxgroup title,!relayer(1)!!maxloop(strlen(title)-3)!!retime("line",0+230-60,0)!{\fs!remember("fs",(908-872)/strinfo("喵", title_line_1_styleref)["height"]*10)!\an2\pos(!title_x+(title_w-title_line_w)/2+remember("swidth",strinfo("喵", title_line_1_styleref)["width"]/10*recall.fs)*(remember("index",j+(j > 2 and 3 or 0))-.5)+remember("gap",(title_line_w-recall.swidth*strlen(title))/(strlen(title)-1))*(recall.index-1)!,!title_line_1_y!)\fn方正兰亭圆_GBK\1c&H000000&\2a&HFF&\K30\fad(0,800)\be.3}!strsub(title,recall.index,1)!

对于第二行下的分割线,实际上我是做成了随机长度的六个矩形。
为了让矩形的角看上去圆滑,我用了\blur0.2,因为这时\be的强度有些偏弱了。
观察原图可以发现六条短线之间有一定的间隔,所以行的总宽度=六个矩形的长度和+五个间隔的长度和,也就是六个矩形的长度和是确定的。
但如果为六个矩形分别随机长度再按照长度和进行缩放,总归会因为小数而弄得不那么完美,所以这里用对于定长进行五个点的分割的方式。
也就是随机0到长度和之间的值五次,对这些“坐标点”进行排序,依据坐标点计算每段的长度,再对最终结果进行打乱。

title_line_w = 1790 - 1464 title_line_3_space = 10 title_line_3_swidth = { title_line_w - 0 - title_line_3_space * 5 } for i = 2, 6 do title_line_3_swidth[i] = math.random(0, title_line_3_swidth[1]) end table.sort(title_line_3_swidth) for i = #title_line_3_swidth, 1, -1 do title_line_3_swidth[i] = title_line_3_swidth[i] - (i == 1 and 0 or title_line_3_swidth[i - 1]) end title_line_3_swidth = shuffle(title_line_3_swidth)

而对于shuffle函数,我们采取Fisher–Yates shuffle的方式:

function shuffle(array) for i = #array, 2, -1 do local j = math.random(i) array[i], array[j] = array[j], array[i] end return array end

而为了能够根据长度计算坐标,再另外写个简单的函数方便使用:

function title_line_3_sum(j) local sum = 0 for i = 1, j do sum = sum + title_line_3_swidth[i] end return sum end

至此,分割线就可以用一行template解决了:

Comment: 0,0:13:09.10,0:13:09.23,Ex-effects,分割线,0,0,0,template line notext fxgroup title,!relayer(1)!!maxloop(6)!!retime("line",0+100-60,0)!{\an7\p1\pos(!title_x+(title_w-remember("line_w",title_line_w-0))/2+title_line_3_sum(j-1)+title_line_3_space*(j-1)!,!title_line_3_y!)\fscx!title_line_3_swidth[j]*100!\1c&H000000&\2a&HFF&\K30\fad(0,800)\blur.2}m 0 -1 l 0 1 l 1 1 l 1 -1

对于「THE ANIMATION」字样,其实原图中具体的运动是向下变速移动+纵向缩放。
所以最好的做法不是用\move而是使用\frz配合\org\t做变速移动,另外再做纵向缩放效果。
在template line中也就是用\org(-100000,0)\frz.004\t(0,200,.5,\frz0)\fscy0\t(0,100,.5,\fscy150)

然后再是对于「に」和「気」字样,这里其实没什么技术含量只有慢慢调整。
大致方向就是\fscx\fscy不断变化来模拟碰撞效果(大红心也是同理):

Comment: 0,0:13:09.23,0:13:09.23,Ex-effects,にと気,0,0,0,template line notext fxgroup title,!relayer(1)!!maxloop(2)!!retime("line",0+230-60,0)!{\fs!remember("fs",(908-872)/strinfo("喵", title_line_1_styleref)["height"]*10)!\an2\pos(!title_x+(title_w-title_line_w)/2+remember("swidth",strinfo("喵", title_line_1_styleref)["width"]/10*recall.fs)*(remember("index",j+3)-.5)+remember("gap",(title_line_w-recall.swidth*strlen(title))/(strlen(title)-1))*(recall.index-1)!,!title_line_1_y!)\fn方正兰亭圆_GBK\1c&H000000&\2a&HFF&\K30\fad(0,800)\t(!730-230+(j == 1 and t1+t2 or t1)*20-100!,!730-230+(j == 1 and t1+t2 or t1)*20!,.5,\fscx110\fscy70\t(!730-230+(j == 1 and t1+t2 or t1)*20!,!730-230+(j == 1 and t1+t2 or t1)*20+100!,\fscx90\fscy110\t(!730-230+(j == 1 and t1+t2 or t1)*20+100!,!730-230+(j == 1 and t1+t2 or t1)*20+200!,\fscx100\fscy100)))\be.3}!strsub(title,recall.index,1)!

然后是「恋」字样。
比较简单的是「恋」字在终点时的弹跳效果,也是能够简单地通过\fscx\fscy\t进行还原。
但「恋」字的运动轨迹怎么看都不像能用标签组合出来的,或者说写出来无异于写一大串一大串的\t硬生生凑出来。
对于这种运动可以每一小段时间生成一行特定的\pos\frz,通过若干行来模拟运动轨迹。
最佳的方式是获取开始时间和结束时间对应的帧及视频的帧率,并逐帧生成(aegisub有一些相关的函数)。但当初写这个效果的时候是每20ms生成一行,算是一个瑕疵了。
至于运动的计算就可以简单按照抛体运动来算竖直和水平分量,不过印象中这里的“重力加速度”每一段有不小差距,只有分别计算(下面的计算也是非常冗杂不知所以然,仅供参考)。

xt = 227.3 if (fxgroup.title_main == true) then xt = 1561.3 end tmp = strinfo("喵", title_line_1_styleref)["width"] / 10 * (908 - 872) / strinfo("喵", title_line_1_styleref)["height"] * 10 x0 = xt + tmp * 2 + (title_line_w - tmp * strlen(title)) / (strlen(title) - 1) * 3 tm = math.ceil((10748 - 9730) / 20) if (fxgroup.title_main == true) then tm = math.ceil((2127 - 860) / 20) end y0 = title_y - 10 ym = title_line_1_y - (908 - 872) + 10 v0x = (xt - x0) / tm t1 = (title_line_w - tmp * strlen(title)) / (strlen(title) - 1) / -v0x t2 = (tmp + (title_line_w - tmp * strlen(title)) / (strlen(title) - 1)) / -v0x t3 = t2 v0y = 0 g1 = (ym - y0) * 2 / t1 ^ 2 y1 = 836 y1 = y1 - (1080 - title_y - title_h) + title_y if (fxgroup.title_main == true) then y1 = 836 end y2 = 848 y2 = y2 - (1080 - title_y - title_h) + title_y if (fxgroup.title_main == true) then y2 = 848 end g2 = (ym - y1) * 2 / (t2 / 2) ^ 2 vc1 = -g2 * (t2 / 2) / (v0y + g1 * t1) g3 = (ym - y2) * 2 / (t3 / 2) ^ 2 vc2 = -g3 * (t3 / 2) / ((v0y + g1 * t1) * vc1 + g2 * t2) g4 = ((title_line_1_y - (908 - 872) / 2) - y2) * 2 / (t3 / 2) ^ 2 function calx(j) local x = x0 + v0x * j return x end function caly(j) local y if (j < t1) then y = y0 + v0y * j + g1 * j ^ 2 * .5 elseif (j < t1 + t2) then local v1y = (v0y + g1 * t1) * vc1 y = ym + v1y * (j - t1) + g2 * (j - t1) ^ 2 * .5 elseif (j < t1 + t2 + t3 / 2) then local v2y = ((v0y + g1 * t1) * vc1 + g2 * t2) * vc2 y = ym + v2y * (j - t1 - t2) + g3 * (j - t1 - t2) ^ 2 * .5 else y = y2 + .5 * g4 * (j - t1 - t2 - t3 / 2) ^ 2 end return y end function calfrz(j) local deg0 = -180 local deg, vdeg if (j < t1) then vdeg = -deg0 / t1 deg = deg0 + vdeg * j elseif (j < t1 + t2) then vdeg = (180 - 0) / t2 deg = 0 + vdeg * (j - t1) else vdeg = (360 - 180) / t3 deg = 180 + vdeg * (j - t1 - t2) end local rdeg = math.abs(deg) rdeg = 180 - math.abs(rdeg - 180) rdeg = 90 - rdeg rdeg = (rdeg / math.abs(rdeg)) * (rdeg / 90) ^ 2 return deg end

在计算坐标之外还需要计算一下\frz,最终在template行就非常简单了:

Comment: 0,0:13:09.73,0:13:09.73,Ex-effects,恋,0,0,0,template line notext fxgroup title,!relayer(2)!!retime("line",0+730-60+tm*20,0)!{\an2\fs!remember("fs",(908-872)/strinfo("喵", title_line_1_styleref)["height"]*10)!\pos(!xt!,!title_line_1_y!)\fn方正兰亭圆_GBK\1c&H6400DE&\t(0,200,.5,\fscx80\fscy110\t(200,300,.5,\fscx100\fscy100))\fad(0,800)\be.3}恋

在「恋」字撞击之后还有一个圆环形波纹,这个波纹用\t还原效果会比较差,所以也是每20ms生成了一行圆环。
首先是如何生成圆形绘图,前文说过如果用两条贝塞尔曲线模拟圆形,也就是用一条贝塞尔曲线拟合半圆时容易穿帮,如果是需要明确表现圆形的情况最好是用一条贝塞尔曲线拟合1/4圆弧,控制点取(2 ^ .5 - 1) * 4 / 3,如果感兴趣可以搜索贝塞尔曲线拟合圆。

function circle(r) local x = r * (2 ^ .5 - 1) * 4 / 3 local c = string.format( "m 0 -%s b -%s -%s -%s -%s -%s 0 b -%s %s -%s %s 0 %s b %s %s %s %s %s 0 b %s -%s %s -%s 0 -%s ", r, x, r, r, x, r, r, x, x, r, r, x, r, r, x, r, r, x, x, r, r) return c end

而为了创建圆弧,可以在圆形绘图之后加上一个翻转的另一直径的圆形绘图,template行也就完成了:

Comment: 0,0:13:10.77,0:13:10.89,Ex-effects,波纹,0,0,0,template line notext fxgroup title,!relayer(3)!!maxloop(120/20)!!retime("preline",0+1770-60+(j-1)*20,0+1770-60+j*20)!{\an7\p1\pos(!xt!,!title_line_1_y-(908 - 872)/2!)\1c&H6400DE&\blur.2}!circle(12+j*2.5)!!Yutils.shape.filter(circle(12-remember("diff",.7)*(120/20)-recall.diff*1.6+j*(2.5+recall.diff)),function(x,y) return _rotate(x,y,0,0,0,0,180,0,0) end)!

最后的最后就是心形烟花,但其实并没有烟花的运动轨迹复杂只是单纯直线运动。
并没有什么复杂的,主要使用\frz\org\t即可实现。

Comment: 0,0:13:10.77,0:13:11.23,Ex-effects,ハート花火,0,0,0,template line notext fxgroup title,!relayer(4)!!maxloop(10)!!retime("preline",0+1770-60,0+1770-60+remember("duration",460-math.random(0,4)*20))!{\an7\p1\pos(!xt!,!title_line_1_y-(908 - 872)/2!)\1c&H5E45F3&\fscx32\fscy32\org(!xt+100000*math.sin(math.rad(remember("theta",math.random(0,360))))!,!title_line_1_y-(908 - 872)/2+100000*math.cos(math.rad(recall.theta))!)\t(0,!recall.duration-100!,.5,\frz.011\t(!recall.duration-100!,!recall.duration!,.5,\frz.012\alpha&HFF&))}!Yutils.shape.filter("m 0 0 b 50 7 42 -27 25 -25 b 27 -42 -7 -50 0 0 ",function(x,y) return _rotate(x,y,0,0,0,0,0,0,45+180-recall.theta) end)!

不过当时写这个效果是用了非常久的,最开始也并没有想把「恋」字做成抛体运动而是做一个非常奇怪的加速度也在曲线变化的运动(但是很好算位移分量),具体当时很多的想法也想不起来了,留下的代码现在让我自己看我也摸不着头脑(甚至函数不知道什么原因都有写重的但我当时一定是因为什么不可知的原因才会保留的吧一定是的)。

题外话

无论如何,我个人对于这些复杂字幕特效的心态是有些消极的。
虽然我自己看到完成的特效或许会非常欣喜,但我知道大部分人并不会在意。
既然字幕特效本身就是不太会被在意的,那么我也没必要把一些不值得的地方的效果做得多好看,只在少数可以做得非常好看、可能会被人注意到并且会让人觉得惊喜的地方做一些用来炫技的特效,或许这样才能让我接受。
 总之,先思考值不值得,不值得就及时放弃。