惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
aimingoo的专栏
aimingoo的专栏
I
InfoQ
B
Blog RSS Feed
D
DataBreaches.Net
S
SegmentFault 最新的问题
P
Proofpoint News Feed
A
About on SuperTechFans
WordPress大学
WordPress大学
Hugging Face - Blog
Hugging Face - Blog
博客园 - 司徒正美
小众软件
小众软件
博客园 - Franky
有赞技术团队
有赞技术团队
D
Docker
T
Tailwind CSS Blog
雷峰网
雷峰网
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Blog — PlanetScale
Blog — PlanetScale
酷 壳 – CoolShell
酷 壳 – CoolShell
B
Blog
V
Visual Studio Blog
宝玉的分享
宝玉的分享
爱范儿
爱范儿

Yesterday17's Blog

2026 新年解密红包 / Melody Flag | Yesterday17's Blog 谈谈 Iori 的设计思路(二):如何实现一个 Showroom 录制工具? | Yesterday17's Blog 谈谈 Iori 的设计思路(一):从 Nico Timeshift 说起 | Yesterday17's Blog Iori Minyami 0.1.0 发布 | Yesterday17's Blog 2025 新年解密红包 / Melody Flag | Yesterday17's Blog 使用 Cloudflare Warp 解决罗森票务的海外登录问题 | Yesterday17's Blog How To Blog 04: The Astro v5 Era | Yesterday17's Blog 谈谈 tokio::select! 的公平性 | Yesterday17's Blog Learning Pingora 05 - Connect with TLS | Yesterday17's Blog Leaving Bytedance | Yesterday17's Blog 大橋彩香 AsiaTour「Reflection」上海公演 个人向记录 & Repo | Yesterday17's Blog Recoving from burnout - What happened? | Yesterday17 Yubikey 重建手册 | Yesterday17's Blog How To Blog 03: Heimus | Yesterday17's Blog 🪧 Blog Migration Accouncement | Yesterday17's Blog Learn Your IDE - VSCode 是如何仅重启插件的? | Yesterday17's Blog How To Blog 02: Astro❤️Password | Yesterday17's Blog How To Blog 01: Why, How, and the Future | Yesterday17's Blog Learning Pingora 04 - Establish L4 Connection | Yesterday17's Blog Learning Pingora 03 - Upstreams and Peers | Yesterday17's Blog Learning Pingora 02 - A Simple HTTP Server | Yesterday17's Blog Learning Pingora 01 - Getting Started | Yesterday17's Blog 2024 新年解密红包 / Melody Flag | Yesterday17's Blog 向新的一年飞驰——记录 2023 | Yesterday17's Blog 「サクラノ刻」对话选摘(2) | Yesterday17's Blog PGP Key Revocation 注销声明 | Yesterday17's Blog 「サクラノ刻」对话选摘(1) | Yesterday17's Blog 2023 新年解密红包 / Melody Flag | Yesterday17's Blog 『蒼の彼方のフォーリズム』通关感想 | Yesterday17's Blog 单显卡直通教程 | Yesterday17's Blog
表达式没有应用卡拉 OK 模板会发生什么?——认识 \t 标签 | Ye...
Yesterday17 · 2020-06-09 · via Yesterday17's Blog

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

ToC

  • 前言
  • 太长不看
  • ! 表达式的处理
  • t2 = 0?
  • 认识 \t
    • 四参数
    • 三参数
    • 二参数
    • 其他参数情况
    • 无视碰撞
    • t2 为 0
    • nested 与速度因子 pwd
    • 忽略错误情况
    • 解析标签
  • 结语

前言

这个问题发生在 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 这个函数进行的,lllong 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 最多是可以接收四个参数的不知道的话去看基本标签整理。简单来说,就是 t1t2acceltags。我们从上到下来看:

四参数

} 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 代表着 t1t2accel 都用到了,因此在这里就不存在默认情况了。

三参数

} else if (cnt == 2) {

t1 = argtoll(args[0]);

t2 = argtoll(args[1]);

accel = 1.;

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

二参数

} else if (cnt == 1) {

t1 = 0;

t2 = 0;

accel = argtod(args[0]);

然后是 cnt == 1 的情况。这种情况下,反而是设置了 accel,而将 t1t0 都设置成了 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 这个平时非常常用但却似乎不大了解的特效标签,也算是有所收获吧(

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