拆分字体/图形的连通区域在许多其它特效工具(After Effects、TCAX)中已经有了现成的实现方式如:
AE拆字脚本:https://www.bilibili.com/video/av3322029
AE的拆字脚本提供了分析过程的控制参数,控制参数会影响到拆分的质量,且依赖于字号大小;从视频上来看,拆分单字用时较长,推测没有直接用字体的矢量信息进行计算,而是用了一些其它的方法来进行连通域的判断,不得不说效率不高。
TCAX(基于Python)的拆字算法:论坛挂了有空补上
利用了一些数学原理,比如叉乘矢量求和判断cw/ccw之类的算法,原则上需要遍历所有的控制点,具体效果和效率也没有办法进行实测。
几年(SuJiKiNen)编写了独立的函数库A3Shape,给Aegisub提供了一些Yutils没有提供的矢量操作函数,使得矢量特效的制作更加多样化。
早在很多年(5+)前,Aegisub的Lua运行环境更换成了LuaJIT,这意味着用Aegisub制作字幕时可以适当地利用现成的C接口。Youka就利用LuaJIT的特性编写了我们熟知的Yutils函数库,它利用系统的字体相关的API来直接获取字体的矢量信息并进行ASS绘图代码化,极大地丰富了字幕特效制作的玩法。
基于上述的Lua函数库,我们可以通过非数学的方式(从代码上看是这样,实际使用的函数多少利用了数学知识),仅仅使用逻辑分析来拆分字体/图形中的连通域,制作新的特效。
利用Yutils自带的函数进行文字转矢量绘图操作,具体代码和效果参考下图:
为了在后面利用矢量绘图,我们把矢量绘图的字符串存为 text, 它包含着多个连通区域;或者我们自己准备一个矢量图形,作为后续分解的对象。
在第四部分我分享了注释过的代码,这里先说明一下注释中使用的概念:
1. 最大化拆分,指的是不考虑图形的时针方向(cw/ccw),拆出所有闭合区域,有正有负;
2. 图形,指连通区域,在下面的过程中大多数是指单连通域;
3. 该图形,指的是循环计数 i 对应的图形;某图形,指的是循环计数 j 对应的图形;
4. 实体点,三次贝塞尔曲线的两个控制点以外的有记录的点;
5. 正确定义的原图形,指的是图形两两间只存在包含和被包含关系;相对地,下图为错误定义的原图形(正常字体设计中不会出现的情况):
这里我们预先准备了一个较为复杂的测试图形:
m 64 -15 l 134 -15 l 134 75 l 64 75 m 87 10 l 87 51 l 111 51 l 111 10 m 18 -8 l 30 -8 l 30 25 l 18 25 m -83 -31 l -70 -31 l -70 -19 l -83 -19 m -74 -13 l -20 -13 l -20 69 l -74 69 m -68 -6 l -68 8 l -28 8 l -28 -6 m -55 12 l -55 46 l -35 46 l -35 12 m -118 -71 l -118 -71 l 180 -71 l 180 111 l -118 111 m 49 -41 l 49 88 l 149 88 l 148 -41 m -96 -42 l -96 86 l -1 86 l -1 -42 m 12 -30 l 12 45 l 35 45 l 35 -30 m 37 -91 l 37 -104 l 166 -104 l 166 -91 m -118 -108 l -118 -88 l 15 -88 l 15 -108
1 2 3 4 5 |
流程: text = split_by_m(text) --原图形最大化拆分 record = rec(text) --记录拆分各部分之间的包含与被包含关系 new_table = record_handler(record,text) --生成新的图形table shape = shape_assembler(new_table) --按已有信息组装新的图形table |
下图为拆分后的图形,共13个单连通域,红色对应后文的情况1,绿色对应情况2,蓝色对应情况3。
各情况举例说明:
情况1——独立图形
① 4号图形的外部图形有10和8,共2个(偶数),且没有内部图形
② 12号图形既没有外部也没有内部图形
情况2——非最外侧的复连通域
① 1号图形的内部有2号图形(1个 大于等于 1),且外部有8号和9号2个(不为0的偶数)图形
情况3——最外侧的复连通域
① 8号图形没有外部图形,内部图形数量不为0
下方shape_assembler函数中,我详细注释了这三种情况下的处理方式。执行一套分析下来,最终可以从13个单连通图形合并得到7个连通域的正确结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
--删除空元素的函数,以防无法按顺序访问table元素 function delete_empty(tbl,mode) if mode == 1 then for i=#tbl,1,-1 do if tbl[i] == "" then _G.table.remove(tbl, i) end end elseif mode==2 then for i=#tbl,1,-1 do if (tbl[i].shape_i == "" or tbl[i].shape == nil) then _G.table.remove(tbl, i) end end end return tbl end |
1 2 3 4 5 6 7 8 9 |
--检查表中是否存在某元素的函数 function is_include(value, tbl) for k,v in _G.ipairs(tbl) do if v == value then return true end end return false end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
--简单的字符串分割函数 function split(str, split_char) local sub_str_tab = {} while true do local pos = string.find(str, split_char) if not pos then _G.table.insert(sub_str_tab,str) break end local sub_str = string.sub(str, 1, pos - 1) _G.table.insert(sub_str_tab,sub_str) str = string.sub(str, pos + 1, string.len(str)) end return sub_str_tab end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
--以 "m" 为标记拆分图形(最大化拆分),并获取部分实体坐标 function split_by_m(text) text = split(text,"m") _G.table.remove(text,1) point = {} for i=1,#text do point[i] = {} text[i] = "m"..text[i] for x,y in string.gmatch(text[i],"[m|l|b] (-?%d+) (-?%d+)") do _G.table.insert(point[i],{x = x,y = y}) end end return text end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
--记录拆分后的图形间包含与被包含关系 function rec(text) new_shape_tbl = {} record = {} for i=1,#text do for j=1,#text do x,y = _G.tonumber(point[j][1].x),_G.tonumber(point[j][1].y) --对于正确定义的原图形,如果某图形中有任意一个实体点落入了该图形中,则该图形包含某图形 if (shape.contains_point(text[i],x,y) and i~=j) then _G.table.insert(record,{inner = j,outer = i}) else _G.table.insert(record,"") end end end return record end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
--处理包含与被包含关系的记录 function record_handler(record_tbl,text) info_tbl = {} outer = {} inner = {} --分析每一条记录 for i=1,#record_tbl do outer[i]={} --记录该图形被哪些图形包含 inner[i]={} --记录该图形包含了哪些图形 for j=1,#record_tbl do if record_tbl[j].inner == i then _G.table.insert(outer[i],record_tbl[j].outer) end if record_tbl[j].outer == i then _G.table.insert(inner[i],record_tbl[j].inner) end end --合成总的信息table:图形编号、外部图形数量,外部图形的编号,内部图形数量,内部图形的编号,图形对应的绘图代码 _G.table.insert(info_tbl,{shape_i = i, o_count = #outer[i],outer = outer[i],in_count = #inner[i],inner=inner[i], shape = text[i]}) end return delete_empty(info_tbl) end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
--依据分析结果重组ass图形 function shape_assembler(info_tbl) new_t = {} delete_empty(info_tbl,2) --删除空记录 _G.table.sort(info_tbl,function (a,b) return a.o_count > b.o_count end) --按照外部图形数量排序,即从最内部的图形开始 info_tbl["n"]= #info_tbl --图形i 后称 该图形 for i=1,info_tbl.n do --第一种情况,如果该图形 没有外部图形且没有内部图形 或 外部图形为偶数个且没有内部图形 这样的图形属于独立图形,无需组合直接保存即可 if ((info_tbl[i].o_count==0 or info_tbl[i].o_count%2==0) and info_tbl[i].in_count==0) then new_t[i] = info_tbl[i] --第二种情况,如果该图形 内部图形数量大于等于1,外部图形数量不为0,且外部图形数量为偶数 这样的图形属于非最外部镂空图形的外圈 elseif (info_tbl[i].in_count>=1 and info_tbl[i].o_count~=0 and info_tbl[i].o_count%2==0) then --从该图形开始查找内圈 for j=i,1,-1 do --如果 该图形 的编号在 某图形 的外部编号表中 if is_include(info_tbl[i].shape_i,info_tbl[j].outer) then info_tbl[i].shape = info_tbl[i].shape.." "..info_tbl[j].shape --连接该图形和某图形 end end new_t[i] = info_tbl[i] --第三种情况,如果该图形 没有外部图形,但是有内部图形 这样的图形是最外圈 elseif (info_tbl[i].o_count==0 and info_tbl[i].in_count~=0) then --从该图形开始查找内圈 for j=i,1,-1 do --如果 该图形 的编号在 某图形 的外部编号表中,并且 某图形 只有1个外部 if is_include(info_tbl[i].shape_i,info_tbl[j].outer) and #info_tbl[j].outer==1 then info_tbl[i].shape = info_tbl[i].shape.." "..info_tbl[j].shape --连接该图形和某图形 end end new_t[i] = info_tbl[i] else new_t[i] ="" end end new_t = delete_empty(new_t,1) --重新生成一个只含有绘图代码的table for i=1,#new_t do new_t[i] = new_t[i].shape end return new_t end |
好久不写这么多东西了,全文没有使用复杂的数学概念,仅靠规律总结和简单逻辑分析判断,达到了拆分矢量图形连通区域的目的。
该方法还没有经过大量的实际验证,但可以确认的是效率较为可观,对于各种字体基本都兼容。如有错误或更好的解决方法望不吝赐教。
战姬绝唱Live 2016中你最喜欢的字幕特效?
投票总人数: 216
≧▽≦ 前排支持~