Skip to content

表达式没有应用卡拉 OK 模板会发生什么?——认识 \t 标签

Published: at 02:20

⚠️ 警告
本文根据 libass 源码编写。尽管 libass 项目以与 VSFilter 项目的兼容性为核心,但不保证 VSFilter 的逻辑和本文所描述的完全一致。

ToC

前言

这个问题发生在 2020 年 6 月 9 日米粒垃圾群森野酱粉丝群的特效入门培训中。涉事代码是这样的:

1
{\an5\frz0\t(0,1500,\frz180)\t(!line.duration-1500!,!line.duration!,\frz360)}123

产生的效果是这样的:

可以看到,前半段还是正常的旋转,但后面就突然变慢了。

产生这个问题的原因很简单:它没有应用卡拉 OK 模板。但在没有应用卡拉 OK 模板的情况下,它究竟被理解成了什么呢?这就是这篇文章探索的内容。

太长不看

先说结论,如果直接只想要标题中这个问题的结果的话,那接下来的文章就不用看了。这一行其实相当于应用下面这个卡拉 OK 模板行的效果:

1
{\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 是怎么定义的:

1
} else if (complex_tag("t")) {
2
double accel;
3
int cnt = nargs - 1;
4
long long t1, t2, t, delta_t;
5
double k;
6
if (cnt == 3) {
7
t1 = argtoll(args[0]);
8
t2 = argtoll(args[1]);
9
accel = argtod(args[2]);
10
} else if (cnt == 2) {
11
t1 = argtoll(args[0]);
12
t2 = argtoll(args[1]);
13
accel = 1.;

也就是说,这里对参数的处理都是交给 argtoll 这个函数进行的,lllong long 的缩写。这里我们不去深究具体是怎么转换的,但是基本是这样的:一系列的 if 都没通过,又没找到合法的数字,于是最后变成了 **0**

好了,现在我们的代码变成了这样:

1
{\an5\frz0\t(0,1500,\frz180)\t(0,0,\frz360)}123

t2 = 0?

第二部分其实相当简单,只需要两行代码就够了:

1
if (t2 == 0)
2
t2 = render_priv->state.event->Duration;

到这里,可以说标题中提出的问题已经解决了。

认识 \t

说是认识 \t,其实也只是认识 libass\t 的实现罢了。我们知道,\t 最多是可以接收四个参数的不知道的话去看基本标签整理。简单来说,就是 t1t2acceltags。我们从上到下来看:

四参数

1
} else if (complex_tag("t")) {
2
double accel;
3
int cnt = nargs - 1;
4
long long t1, t2, t, delta_t;
5
double k;
6
if (cnt == 3) {
7
t1 = argtoll(args[0]);
8
t2 = argtoll(args[1]);
9
accel = argtod(args[2]);

首先是 cnt == 3 的情况,这里的 cnt 指的是除最后一个参数之外的参数数量。3 代表着 t1t2accel 都用到了,因此在这里就不存在默认情况了。

三参数

1
} else if (cnt == 2) {
2
t1 = argtoll(args[0]);
3
t2 = argtoll(args[1]);
4
accel = 1.;

接下来是 cnt == 2 的情况。cnt == 2 意味着指定了 t1t2,而这里的 accel,则被设置成了默认值 1

二参数

1
} else if (cnt == 1) {
2
t1 = 0;
3
t2 = 0;
4
accel = argtod(args[0]);

然后是 cnt == 1 的情况。这种情况下,反而是设置了 accel,而将 t1t0 都设置成了 0。

其他参数情况

1
} else {
2
t1 = 0;
3
t2 = 0;
4
accel = 1.;
5
}

最后是其他情况。这种情况下,所有值都会采用默认,也就是上面默认值的综合。

无视碰撞

1
render_priv->state.detect_collisions = 0;

对于 \t 而言,由于是与时间轴密切相关的效果,因此关闭了碰撞检测。于是这里就出现了一个可以利用的点:使用错误的 \t 关闭碰撞检测,以代替某些 \pos 的使用场景。

其实这里字是重叠的
其实这里字是重叠的

t2 为 0

1
if (t2 == 0)
2
t2 = render_priv->state.event->Duration;

这就是上面说过的了,这里略过。

nested 与速度因子 pwd

1
delta_t = t2 - t1;
2
t = render_priv->time - render_priv->state.event->Start; // FIXME: move to render_context
3
if (t < t1)
4
k = 0.;
5
else if (t >= t2)
6
k = 1.;
7
else {
8
assert(delta_t != 0.);
9
k = pow(((double) (t - t1)) / delta_t, accel);
10
}
11
if (nested)
12
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

忽略错误情况

1
if (cnt < 0 || cnt > 3)
2
continue;

对于参数数量不正确的情况,直接略过下面的处理。利用无视碰撞更方便了呢(逃

解析标签

1
p = args[cnt].start;
2
if (args[cnt].end < end) {
3
p = parse_tags(render_priv, p, args[cnt].end, k, true);
4
} else {
5
assert(q == end);
6
// No other tags can possibly follow this \t tag,
7
// so we don't need to restore pwr after parsing \t.
8
// The recursive call is now essentially a tail call,
9
// so optimize it away.
10
pwr = k;
11
nested = true;
12
q = p;
13
}

最后就是解析标签的步骤了,值得注意的是这里针对 end 的情况作了简单处理。

到这里为止,对 \k 的分析也就基本完成了。

结语

我个人是比较喜欢这种说来就来的旅行(分析)的。发现问题——解决问题——挖掘问题——总结问题,这样的学习过程给人带来的正反馈是最强的。

借着这个机会,或者说是契机吧,我简单了解了 \t 这个平时非常常用但却似乎不大了解的特效标签,也算是有所收获吧(

一时兴起,也就没考虑太多东西。有用就好(笑)