警告
本文根据 libass 源码编写。尽管 libass 项目以与 VSFilter 项目的兼容性为核心,但不保证 VSFilter 的逻辑和本文所描述的完全一致。
前言
这个问题发生在 2020 年 6 月 9 日米粒垃圾群森野酱粉丝群的特效入门培训中。涉事代码是这样的:
{\an5\frz0\t(0,1500,\frz180)\t(!line.duration-1500!,!line.duration!,\frz360)}123
产生的效果是这样的:
可以看到,前半段还是正常的旋转,但后面就突然变慢了。
产生这个问题的原因很简单:它没有应用卡拉 OK 模板。但在没有应用卡拉 OK 模板的情况下,它究竟被理解成了什么呢?这就是这篇文章探索的内容。
太长不看
先说结论,如果直接只想要标题中这个问题的结果的话,那接下来的文章就不用看了。这一行其实相当于应用下面这个卡拉 OK 模板行的效果:
{\an5\frz0\t(0,1500,\frz180)\t(0,!line.duration!,\frz360)}
如果你对这个结论如何出现有所好奇的话,那接下来的部分就是为你而准备的了(笑)
!
表达式的处理
对这个问题的探究总共分为两部分,第一部分就是 !
表达式的处理了。在 \t
中,!line.duration!
这样的代码究竟被理解成了什么是这一部分需要探究的重点。
我们知道不知道的话去看 kara-templater 那篇文章,在 kara-templater
的作用下,!line.duration!
实际上是被替换成了 return (line.duration)
的运算结果。但在没有 kara-templater
的情景下,这样的替换显然是不存在的。也就是说,!line.duration!
被完整地当成了一个 \t
的参数。我们来看 libass
对 \t
是怎么定义的:
} else if (complex_tag("t")) { double accel; int cnt = nargs - 1; long long t1, t2, t, delta_t; double k; if (cnt == 3) { t1 = argtoll(args[0]); t2 = argtoll(args[1]); accel = argtod(args[2]); } else if (cnt == 2) { t1 = argtoll(args[0]); t2 = argtoll(args[1]); accel = 1.;
也就是说,这里对参数的处理都是交给 argtoll
这个函数进行的,ll
是 long long
的缩写。这里我们不去深究具体是怎么转换的,但是基本是这样的:一系列的 if
都没通过,又没找到合法的数字,于是最后变成了 0
。
好了,现在我们的代码变成了这样:
{\an5\frz0\t(0,1500,\frz180)\t(0,0,\frz360)}123
t2 = 0?
第二部分其实相当简单,只需要两行代码就够了:
if (t2 == 0) t2 = render_priv->state.event->Duration;
到这里,可以说标题中提出的问题已经解决了。
认识 \t
说是认识 \t
,其实也只是认识 libass
中 \t
的实现罢了。我们知道,\t
最多是可以接收四个参数的不知道的话去看基本标签整理。简单来说,就是 t1
、t2
、accel
和 tags
。我们从上到下来看:
四参数
} else if (complex_tag("t")) { double accel; int cnt = nargs - 1; long long t1, t2, t, delta_t; double k; if (cnt == 3) { t1 = argtoll(args[0]); t2 = argtoll(args[1]); accel = argtod(args[2]);
首先是 cnt == 3
的情况,这里的 cnt
指的是除最后一个参数之外的参数数量。3
代表着 t1
、t2
和 accel
都用到了,因此在这里就不存在默认情况了。
三参数
} else if (cnt == 2) { t1 = argtoll(args[0]); t2 = argtoll(args[1]); accel = 1.;
接下来是 cnt == 2
的情况。cnt == 2
意味着指定了 t1
和 t2
,而这里的 accel
,则被设置成了默认值 1
。
二参数
} else if (cnt == 1) { t1 = 0; t2 = 0; accel = argtod(args[0]);
然后是 cnt == 1
的情况。这种情况下,反而是设置了 accel
,而将 t1
和 t0
都设置成了 0。
其他参数情况
} else { t1 = 0; t2 = 0; accel = 1.; }
最后是其他情况。这种情况下,所有值都会采用默认,也就是上面默认值的综合。
无视碰撞
render_priv->state.detect_collisions = 0;
对于 \t
而言,由于是与时间轴密切相关的效果,因此关闭了碰撞检测。于是这里就出现了一个可以利用的点:使用错误的 \t
关闭碰撞检测,以代替某些 \pos
的使用场景。
t2
为 0
if (t2 == 0) t2 = render_priv->state.event->Duration;
这就是上面说过的了,这里略过。
nested
与速度因子 pwd
delta_t = t2 - t1; t = render_priv->time - render_priv->state.event->Start; // FIXME: move to render_context if (t < t1) k = 0.; else if (t >= t2) k = 1.; else { assert(delta_t != 0.); k = pow(((double) (t - t1)) / delta_t, accel); } if (nested) pwr = k;
接下来这部分就比较特殊了。我们知道,\t
是可以指定 accel
的,而这个 accel
也就必须传入到对 \t
中使用的标签的解析中。
对于 t
小于 t1
的,其实就可以直接不显示了,这里将 k
设为 0。
对于 t
大于 t2
的,需要原样显示,因此 k
设为 1。
对于 t
位于 t1 ~ t2
的,需要按照指定的 accel
设置速度。设置公式为:
对于 nested
的情况,也就是目前的 \t
标签就在 \t
标签里的情况,这里直接将 pwr
覆盖为刚才计算出的 k
,也就是以内部 \t
的速度为准,不叠加 accel
。
忽略错误情况
if (cnt < 0 || cnt > 3) continue;
对于参数数量不正确的情况,直接略过下面的处理。利用无视碰撞更方便了呢(逃
解析标签
p = args[cnt].start; if (args[cnt].end < end) { p = parse_tags(render_priv, p, args[cnt].end, k, true); } else { assert(q == end); // No other tags can possibly follow this \t tag, // so we don't need to restore pwr after parsing \t. // The recursive call is now essentially a tail call, // so optimize it away. pwr = k; nested = true; q = p; }
最后就是解析标签的步骤了,值得注意的是这里针对 end
的情况作了简单处理。
到这里为止,对 \k
的分析也就基本完成了。
结语
我个人是比较喜欢这种说来就来的旅行(分析)的。发现问题——解决问题——挖掘问题——总结问题,这样的学习过程给人带来的正反馈是最强的。
借着这个机会,或者说是契机吧,我简单了解了 \t
这个平时非常常用但却似乎不大了解的特效标签,也算是有所收获吧(
一时兴起,也就没考虑太多东西。有用就好(笑)