[魔典] 日本动画字幕制作 #04.1 奏心象之風景
番外

番外篇就懒得拘束了,标题乱取的。

本篇将试着做一些简单的特效,如果看得懂,那说明你没救了()
珍爱生命,酌情跳过番外。

正篇

就连我自己都想吐槽系列标题“日本动画字幕制作”
至少事实而言,近来的大部分内容多少都脱线了
——但不碍事,还有更脱线的

以下默认您已经熟知……和……和……和…………等等等等
默认您已经准备好浪费生命
默认您已经做好了心理准备
默认您不是熟悉特效制作只来看乐子的
默认您能接受代码风格的差异

番外篇或在后续追加、修改内容

不K轴分块

如果想在不K轴时逐字做特效,最简单的方法就是template syl char(事先为所有的无K轴字幕至少开头打一个{\k233})

但是这样做的弊端还是很明显的,逐字可能会没有整体感,并且字与字间难以产生联动
且,逐字可能会产生过多行,导致观看mkv时字幕卡顿——不要说不会卡顿,无论自己的设备如何也得考虑兼容其他设备,并且减低滤镜渲染压力也是非常好的

于是,就当有这样一个不K轴但为字幕每几个字分块处理的需求,以下也就是个人来实现这个需求的想法

假定目标字幕为【萌え萌えキュンです】

字节处理

首先一个问题,如果试试#"萌え萌えキュンです",lua会输出27

???
这是为什么?

因为lua计算的是字节数,并非字符数,并且lua没有提供计算utf8的字符数的方式
utf8:感兴趣自行了解

但总之根据utf8的规则,lua的曲线实现获取字符长度的方法如下:

function strlen(str) local tmp = str local count = 0 local byteCount = 0 while (string.len(tmp) > 0) do local code = string.byte(tmp) if (code <= 127) then byteCount = byteCount + 1 elseif (code <= 223) then byteCount = byteCount + 2 elseif (code <= 239) then byteCount = byteCount + 3 else byteCount = byteCount + 4 end tmp = string.sub(str, byteCount + 1) count = count + 1 end return count end

function strlen(str)
    local tmp = str
    local count = 0
    local byteCount = 0
    while (string.len(tmp) > 0) do
        local code = string.byte(tmp)
        if (code <= 127) then
            byteCount = byteCount + 1
        elseif (code <= 223) then
            byteCount = byteCount + 2
        elseif (code <= 239) then
            byteCount = byteCount + 3
        else
            byteCount = byteCount + 4
        end
        tmp = string.sub(str, byteCount + 1)
        count = count + 1
    end
    return count
end

根据string.byte获得到的编码判定字符的字节长度,然后简单循环一下获得utf8下的字符长度

然后就是截取字符串了,同上缘由,lua自有的string.sub满足不了需求,故需另行实现:

function strsub(str, start, length) if (length <= 0) then return "" end local tmp = str local count = 0 local byteCount = 0 local byteSubStart = 1 local byteSubEnd = -1 while (string.len(tmp)>0) do if (count == start) then byteSubStart = byteCount + 1 elseif (count == start + length) then byteSubEnd = byteCount break end local code = string.byte(tmp) if(code <= 127) then byteCount = byteCount +1 elseif (code <= 223) then byteCount = byteCount + 2 elseif (code <= 239) then byteCount = byteCount + 3 else byteCount = byteCount + 4 end tmp = string.sub(str, byteCount+1) count = count + 1 end return string.sub(str, byteSubStart, byteSubEnd) end

function strsub(str, start, length)
    if (length <= 0) then
        return ""
    end
    local tmp = str
    local count = 0
    local byteCount = 0
    local byteSubStart = 1
    local byteSubEnd = -1
    while (string.len(tmp) > 0) do
        if (count == start) then
            byteSubStart = byteCount + 1
        elseif (count == start + length) then
            byteSubEnd = byteCount
            break
        end
        local code = string.byte(tmp)
        if (code <= 127) then
            byteCount = byteCount + 1
        elseif (code <= 223) then
            byteCount = byteCount + 2
        elseif (code <= 239) then
            byteCount = byteCount + 3
        else byteCount = byteCount + 4
        end
        tmp = string.sub(str, byteCount + 1)
        count = count + 1
    end
    return string.sub(str, byteSubStart, byteSubEnd)
end

新建code once行,设slen为4(Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,code once,slen = 4)

然后新建template line notext行,输入!maxloop(remember("loop",math.ceil(strlen(line.text_stripped)/slen)))!!strsub(line.text_stripped, (j-1)*slen, slen)!

运行后就会生成三行,分别为萌え萌え、キュンで、す
其中line.text_stripped是获取当前行的文字(不含标签)

然后问题来了,如何获得【萌え萌え】的横坐标呢?

没有音节,没有syl;使用的是line,获得的$center、$left、$right都是整行的
几乎无法人为计算每个字符的长度(废话)

当然,解决方法是肯定存在的,因为aegisub本身能够计算syl的$center之类的,说明本身是有办法的
这种时候假定你不知道有什么可能的函数,就应该先去看看文档,不过这时更奏效的是看看源文件karaskel-auto4.lua

总之加入你知道了需要用aegisub.text_extents(styleref, text)来操作
传入两个参数,一个是可以通过line.styleref获得到的,一个是要处理的字符串
返回四个值,width、height、descent、extlead

所以新建code once行,输入function getSinfo(styleref,str) local width, height, descent, extlead = _G.aegisub.text_extents(styleref,str) return width end
因为只进行横向处理,所以我们只要width

那么计算横坐标的方式就可以有了:
!$lleft+getSinfo(line.styleref, strsub(line.text_stripped, 0, (j-1)*slen))+getSinfo(line.styleref, remember("stext", strsub(line.text_stripped, (j-1)*slen, slen)))/2!

总体参考:
Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,code once,slen = 4
Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,code once,function getSinfo(styleref,str) local width, height, descent, extlead = _G.aegisub.text_extents(styleref,str) return width end
Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,code once,function strlen(str) local tmp = str local count = 0 local byteCount = 0 while (string.len(tmp) > 0) do local code = string.byte(tmp) if (code <= 127) then byteCount = byteCount + 1 elseif (code <= 223) then byteCount = byteCount + 2 elseif (code <= 239) then byteCount = byteCount + 3 else byteCount = byteCount + 4 end tmp = string.sub(str, byteCount + 1) count = count + 1 end return count end
Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,code once,function strsub(str, start, length) if (length <= 0) then return "" end local tmp = str local count = 0 local byteCount = 0 local byteSubStart = 1 local byteSubEnd = -1 while (string.len(tmp)>0) do if (count == start) then byteSubStart = byteCount + 1 elseif (count == start + length) then byteSubEnd = byteCount break end local code = string.byte(tmp) if(code <= 127) then byteCount = byteCount +1 elseif (code <= 223) then byteCount = byteCount + 2 elseif (code <= 239) then byteCount = byteCount + 3 else byteCount = byteCount + 4 end tmp = string.sub(str, byteCount+1) count = count + 1 end return string.sub(str, byteSubStart, byteSubEnd) end
Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,template line notext,!maxloop(remember("loop",math.ceil(strlen(line.text_stripped)/slen)))!{\an5\pos(!$lleft+getSinfo(line.styleref, strsub(line.text_stripped, 0, (j-1)*slen))+getSinfo(line.styleref, remember("stext", strsub(line.text_stripped, (j-1)*slen, slen)))/2!,$middle)}!recall.stext!
Comment: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,karaoke,萌え萌えキュンです

生成:
Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,fx,{\an5\pos(691.8125,1010)}萌え萌え
Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,fx,{\an5\pos(1121.4375,1010)}キュンで
Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,fx,{\an5\pos(1389.953125,1010)}す
注:由于字体和配置的不一,所以这里的坐标仅供参考,可以看出是有效的

优化:实际坐标可能并不需要那么精确,简单的话就直接取整
但要实现复杂或较特殊的特效时,坐标还是精确到小数点后两位比较好,设一个函数或者直接math.ceil(x*100)/100也行

简单做一个特效

先别急着被下面一大堆给吓到了,实际而言下面的实现大部分取的就是上面的几个函数
甚至template行根本就是很随性地写的,简洁的话下面的特效应该只需要一行template

Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,code once,slen = 3 schange = 640
Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,code once,function getSinfo(styleref,str) local width, height, descent, extlead = _G.aegisub.text_extents(styleref,str) return width end
Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,code once,function strlen(str) local tmp = str local count = 0 local byteCount = 0 while (string.len(tmp) > 0) do local code = string.byte(tmp) if (code <= 127) then byteCount = byteCount + 1 elseif (code <= 223) then byteCount = byteCount + 2 elseif (code <= 239) then byteCount = byteCount + 3 else byteCount = byteCount + 4 end tmp = string.sub(str, byteCount + 1) count = count + 1 end return count end
Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,code once,function strsub(str, start, length) if (length <= 0) then return "" end local tmp = str local count = 0 local byteCount = 0 local byteSubStart = 1 local byteSubEnd = -1 while (string.len(tmp)>0) do if (count == start) then byteSubStart = byteCount + 1 elseif (count == start + length) then byteSubEnd = byteCount break end local code = string.byte(tmp) if(code <= 127) then byteCount = byteCount +1 elseif (code <= 223) then byteCount = byteCount + 2 elseif (code <= 239) then byteCount = byteCount + 3 else byteCount = byteCount + 4 end tmp = string.sub(str, byteCount+1) count = count + 1 end return string.sub(str, byteSubStart, byteSubEnd) end
Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,template line notext,!maxloop(remember("loop",math.ceil(strlen(line.text_stripped)/slen)))!!relayer(math.random(1,3))!!retime("line",0+math.random(0,120),0+420)!{\an5\pos(!remember("left", math.ceil(($lleft+getSinfo(line.styleref, strsub(line.text_stripped, 0, (j-1)*slen)))*100)/100)+remember("width", math.ceil((getSinfo(line.styleref, remember("stext", strsub(line.text_stripped, (j-1)*slen, slen))))*100)/100)/2!,$middle)\1c!_G.ass_color(_G.HSV_to_RGB(math.random(0,360*.6),1,1))!\clip(!math.random(0,recall.width)*.5+recall.left!,!remember("side", math.random(0,1))*$ltop+(1-recall.side)*$lbottom!,!math.random(0,recall.width)*.5+recall.width/2+recall.left!,!$ltop*recall.side+$lbottom*(1-recall.side)!)\t(0,!schange!,1.2,\clip(!recall.left!,$ltop,!recall.left+recall.width!,$lbottom)\t(!schange!,!schange*1.4!,\1c&HFFFFFF&))}!recall.stext!
Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,template line notext,!maxloop(remember("loop",math.ceil(strlen(line.text_stripped)/slen)))!!relayer(math.random(1,3))!!retime("line",0+math.random(0,120),0-$ldur+schange*1.4)!{\an5\pos(!remember("left", math.ceil(($lleft+getSinfo(line.styleref, strsub(line.text_stripped, 0, (j-1)*slen)))*100)/100)+remember("width", math.ceil((getSinfo(line.styleref, remember("stext", strsub(line.text_stripped, (j-1)*slen, slen))))*100)/100)/2!,$middle)\1c!_G.ass_color(_G.HSV_to_RGB(math.random(0,360*.6),1,1))!\clip(!recall.left!,!math.random(0,$lheight)/2+$ltop!,!recall.left!,!$lbottom-math.random(0,$lheight)/2!)\t(0,!schange!,1.2,\clip(!recall.left!,$ltop,!recall.left+recall.width!,$lbottom))\fad(0,!schange*.4!)}!recall.stext!
Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,template line notext,!maxloop(remember("loop",math.ceil(strlen(line.text_stripped)/slen)))!!relayer(math.random(1,3))!!retime("line",0+math.random(0,120),0-$ldur+schange*1.4)!{\an5\pos(!remember("left", math.ceil(($lleft+getSinfo(line.styleref, strsub(line.text_stripped, 0, (j-1)*slen)))*100)/100)+remember("width", math.ceil((getSinfo(line.styleref, remember("stext", strsub(line.text_stripped, (j-1)*slen, slen))))*100)/100)/2!,$middle)\1c!_G.ass_color(_G.HSV_to_RGB(math.random(0,360*.6),1,1))!\clip(!recall.left+recall.width!,!math.random(0,$lheight)/2+$ltop!,!recall.left+recall.width!,!$lbottom-math.random(0,$lheight)/2!)\t(0,!schange!,1.2,\clip(!recall.left!,$ltop,!recall.left+recall.width!,$lbottom))\fad(0,!schange*.4!)}!recall.stext!
Comment: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,karaoke,萌え萌えキュンです

从中挑出一行:
Comment: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,template line notext,!maxloop(remember("loop",math.ceil(strlen(line.text_stripped)/slen)))!!relayer(math.random(1,3))!!retime("line",0+math.random(0,120),0-$ldur+schange*1.4)!{\an5\pos(!remember("left", math.ceil(($lleft+getSinfo(line.styleref, strsub(line.text_stripped, 0, (j-1)*slen)))*100)/100)+remember("width", math.ceil((getSinfo(line.styleref, remember("stext", strsub(line.text_stripped, (j-1)*slen, slen))))*100)/100)/2!,$middle)\1c!_G.ass_color(_G.HSV_to_RGB(math.random(0,360*.6),1,1))!\clip(!recall.left!,!math.random(0,$lheight)/2+$ltop!,!recall.left!,!$lbottom-math.random(0,$lheight)/2!)\t(0,!schange!,1.2,\clip(!recall.left!,$ltop,!recall.left+recall.width!,$lbottom))\fad(0,!schange*.4!)}!recall.stext!

template line notext,也就是以line为对象,去掉原有的字而在结尾用上面的方式取出3(slen)个字

maxloop(remember("loop",math.ceil(strlen(line.text_stripped)/slen)))
获得文字长度除以3再取整,记为"loop"方便之后根据循环总数而调整参数

relayer(math.random(1,3))
随机层数,以达到生成出的三行能够互相覆盖

retime("line",0+math.random(0,120),0-$ldur+schange*1.4)
按line的形式调整开始时间和结束时间,开始时间随机错开一小段,结束时间为schange的1.4倍
但实际按照实现的效果而言此处是可以优化的

然后就是{}开始的\an5、\pos一大段,都是和上面一致的

\1c!_G.ass_color(_G.HSV_to_RGB(math.random(0,360*.6),1,1))!
随机颜色,之所以h不取0-360是因为.6网上的紫色在此特效下非常不搭

\clip(!recall.left!,!math.random(0,$lheight)/2+$ltop!,!recall.left!,!$lbottom-math.random(0,$lheight)/2!)
随机在处理的3个字的左右两边clip一段,也就是初始时不显示

\t(0,!schange!,1.2,\clip(!recall.left!,$ltop,!recall.left+recall.width!,$lbottom))
通过\t使clip变化至3个字的左上到右下,也就是从初始开始逐渐显示出整个部分

\fad(0,!schange*.4!)
结束前逐渐消失,为第一行template腾出颜色

在调整过字体、字体大小、边框、阴影、颜色后的效果图:

template行部分实际写得太随性了,以要实现的效果而言一行就够了
而且还有不少可以优化的部分,但就留给读者想象了
效果图中的第二个萌字的右边被截掉了,这时可能想到的就是坐标是不是多次四舍五入,内联变量$lleft也是取整导致的;但实际这些四舍五入还原回去得出来的坐标值也最多差两个像素不到,直接点掉原本的karaoke行就能对比,实际单纯字体问题

To be Continued

后续内容待补充

空白

少女还未记录主人的信息,或许需要登录……