当前位置:首 页 > 字幕教程 > 查看文章

一、背景和需求分析

  拆分字体/图形的连通区域在许多其它特效工具(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部分

  利用Yutils自带的函数进行文字转矢量绘图操作,具体代码和效果参考下图:

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

测试原图形
流程:
  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个连通域的正确结果。

正确拆分的结果

四、连通区域拆分使用的函数

--删除空元素的函数,以防无法按顺序访问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
--检查表中是否存在某元素的函数
function is_include(value, tbl)
    for k,v in _G.ipairs(tbl) do
      if v == value then
          return true
      end
    end
    return false
end
--简单的字符串分割函数
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
--以 "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
--记录拆分后的图形间包含与被包含关系
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
--处理包含与被包含关系的记录
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
--依据分析结果重组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

五、结语

  好久不写这么多东西了,全文没有使用复杂的数学概念,仅靠规律总结和简单逻辑分析判断,达到了拆分矢量图形连通区域的目的。
  该方法还没有经过大量的实际验证,但可以确认的是效率较为可观,对于各种字体基本都兼容。如有错误或更好的解决方法望不吝赐教。
  

Vmoe特效君一枚......欢迎各位参观官网......探讨特效和Aegisub.......

—— DOMO

评论Comments
  1. 1 楼 cmdhout

    ≧▽≦ 前排支持~

    2019年03月30日 16:31:52 回复

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理

  • 评论最多
  • 最新评论
  • 随机文章

投票

战姬绝唱Live 2016中你最喜欢的字幕特效?

  • M1 Exterminate (8%, 30 张票)
  • M2 星天ギャラクシィクロス (3%, 11 张票)
  • M3 不死鳥のフランメ (7%, 26 张票)
  • M4 SENSE OF DISTANCE (1%, 5 张票)
  • M5 Just Loving X-Edge (5%, 16 张票)
  • M6 Edge works of Goddess ZABABA (3%, 10 张票)
  • M7 ORBITAL BEAT (ver.ZABABA) (1%, 4 张票)
  • M8 おきてがみ (3%, 9 张票)
  • M9 陽だまりメモリア (0%, 0 张票)
  • M10 歪鏡・シェンショウジン (3%, 9 张票)
  • M11 銀腕・アガートラーム (1%, 4 张票)
  • M12 純白イノセント (3%, 9 张票)
  • M13 Rebirth-day (1%, 4 张票)
  • M14 TRUST HEART (1%, 5 张票)
  • M15 繋いだ手だけが紡ぐもの (1%, 4 张票)
  • M16 放課後キーホルダー (2%, 7 张票)
  • M17 BAYONET CHARGE (2%, 6 张票)
  • M18 Beyond the BLADE (2%, 8 张票)
  • M19 空へ… (8%, 27 张票)
  • M20 Glorious Break (2%, 8 张票)
  • M21 殲琴・ダウルダブラ (4%, 13 张票)
  • M22 tomorrow (2%, 7 张票)
  • M23 リトルミラクル -Grip it tight- (1%, 4 张票)
  • M24 限界突破G-beat (1%, 4 张票)
  • M25 撃槍・ガングニール (2%, 7 张票)
  • M26 いつかの虹、花の想い出 (3%, 12 张票)
  • M27 始まりの歌(バベル) (6%, 23 张票)
  • EN1 RADIANT FORCE (6%, 21 张票)
  • EN2 「ありがとう」を唄いながら (5%, 19 张票)
  • EN3 FIRST LOVE SONG (2%, 8 张票)
  • EN4 虹色のフリューゲル (10%, 36 张票)

投票总人数: 216

加载中 ... 加载中 ...
footer logo
未经许可请勿自行将本网站之内容用于商业用途
Copyright ©2011-2026 Vmoe All Rights Reserved. Powered by WordPress. Theme modified from QQOQ | RSS | 隐私政策 |