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

推荐订阅源

S
Secure Thoughts
宝玉的分享
宝玉的分享
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
爱范儿
爱范儿
Recorded Future
Recorded Future
博客园 - 【当耐特】
博客园 - 聂微东
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Apple Machine Learning Research
Apple Machine Learning Research
GbyAI
GbyAI
MongoDB | Blog
MongoDB | Blog
S
SegmentFault 最新的问题
C
Check Point Blog
N
Netflix TechBlog - Medium
阮一峰的网络日志
阮一峰的网络日志
Microsoft Azure Blog
Microsoft Azure Blog
Last Week in AI
Last Week in AI
博客园 - 叶小钗
Google DeepMind News
Google DeepMind News
罗磊的独立博客
T
The Blog of Author Tim Ferriss
Recent Announcements
Recent Announcements
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Jina AI
Jina AI
The Cloudflare Blog
博客园 - 司徒正美
D
Docker
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
J
Java Code Geeks
B
Blog
Martin Fowler
Martin Fowler
L
LangChain Blog
V
Visual Studio Blog
U
Unit 42
P
Proofpoint News Feed
Vercel News
Vercel News
Hugging Face - Blog
Hugging Face - Blog
腾讯CDC
F
Fortinet All Blogs
M
MIT News - Artificial intelligence
人人都是产品经理
人人都是产品经理
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
The Register - Security
The Register - Security
大猫的无限游戏
大猫的无限游戏
D
DataBreaches.Net
T
Tailwind CSS Blog
H
Help Net Security
IT之家
IT之家
酷 壳 – CoolShell
酷 壳 – CoolShell

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 对博客与笔记的思考 | Yesterday17's Blog Project Anni 之旅(3)自动化 Flutter 应用 CI/CD 上架流程 | Yesterday17's Blog AsobiStage 直接播放链接 | Yesterday17 如何在后分P时代进行投稿——sswa使用详解 | Yesterday17's Blog JSON RPC 与 LSP 协议基础 | Yesterday17's Blog Grajapa Shueisha / BookEnd 加密方式调查 | Yesterday17's Blog 【2022篇+WriteUp】如何再收一个新年红包? | Yesterday17's Blog 如何将良心云的良心功能清理干净 | Yesterday17's Blog 【油猴脚本】bilibili 投稿页面返回旧版+旧版页面强制允许分P上传 | Yesterday17's Blog Cloudr1v1 授权方式分析 | Yesterday17 Typora 1.0.2 逆向实录 | Yesterday17's Blog Project Anni 之旅(2)ValueAfterTable——toml-rs的实现与限制 | Yesterday17's Blog IPv4透明代理+IPv6 Passthrough——树莓派单臂软路由折腾记 | Yesterday17's Blog Chaos; Child 汉化补丁 神秘编码探索 | Yesterday17's Blog 镣铐与舞蹈——个性与共性之迷思 | Yesterday17's Blog Go 学习笔记 02 - 找准 io 之道 | Yesterday17's Blog NAT Slipstreaming v1 原理浅析 | Yesterday17's Blog 绕过「9-nine-」的 CDKEY 验证——KrkrPlugin 正(?)向实录 | Yesterday17's Blog 静流的青春纪念册——「サクラノ刻 -櫻の森の下を歩む-」体验版感言 | Yesterday17's Blog Project Anni 之旅 01 - 从 clap-builder 到 derive | Yesterday17's Blog [Google CTF 2021] CPP WriteUp | Yesterday17's Blog 获取 アソビステージ 的实际播放链接 | Yesterday17's Blog 90 行 Rust 代码实现 AsyncTeeReader | Yesterday17's Blog 或许还算有价值一读的文章列表 | Yesterday17's Blog 从零开始的 Seedbox 之旅 | Yesterday17's Blog [随笔]技术型博客行文迷思(1) | Yesterday17's Blog 浅谈 git fetch 的工作方式 | Yesterday17's Blog 『ソーサレス*アライヴ! ~the World's End Fallen Star~』通关感想" | Yesterday17's Blog Rust std::fmt 格式语法简述 | Yesterday17's Blog 日亚修改居住国的解决方案 | Yesterday17's Blog [Windows/Linux] GC553 的 Switch 完美采集之路 | Yesterday17's Blog 【翻译】Subtyping and Variance / 子类型与变型 | Yesterday17's Blog Berd's Red Envelope 2021 WriteUp | Yesterday17's Blog 【中英对照】ALSA 音频 API 使用教程/A Tutorial on Using the ALSA Audio API | Yesterday17's Blog 从 cue_scanner.l 看 CUE Sheet 的词法单元 | Yesterday17's Blog Postman 历史记录导出的解决方案 | Yesterday17's Blog 《恋爱绮谭 不存在的夏天》通关感想 | Yesterday17's Blog [微机实验/TD-PITE] 微机接口综合实验 | Yesterday17's Blog [微机实验/TD-PITE] 键盘扫描及数码管显示实验 | Yesterday17's Blog [微机实验/TD-PITE] 数码管显示实验 | Yesterday17's Blog Airsonic Advanced+Google Drive+Caddy 部署纪实 | Yesterday17's Blog X-NUCA 2020 - hellowasm 题解 | Yesterday17's Blog [微机实验/TD-PITE] 8251 串行接口实验 | Yesterday17's Blog Node.js child_process.fork 与 env 污染 RCE | Yesterday17's Blog EP.01 「夜の向日葵」 | Yesterday17's Blog [微机实验/TD-PITE] 8254 定时/计数器实验+选做实验 | Yesterday17's Blog [JLU CTF/2020] babywasm WriteUp | Yesterday17's Blog PHP 反序列化与经典利用 | Yesterday17's Blog WebAssembly 逆向简述 | Yesterday17's Blog 『彼女、お借りします』一期完结点评 | Yesterday17's Blog [微机实验/TD-PITE] D/A 转换实验+选做实验 | Yesterday17's Blog [微机实验/TD-PITE] A/D 转换实验+选做实验 | Yesterday17's Blog 开源项目申请 JetBrains Open Source License 简单流程 | Yesterday17's Blog 微软拼音与 JetBrains 搜索快捷键冲突的解决方案 | Yesterday17's Blog [微机实验/TD-PITE] 8259 中断优先级实验+选做实验 | Yesterday17's Blog IFTTT 测试(续) | Yesterday17 IFTTT 测试 | Yesterday17's Blog [微机实验/TD-PITE] 存储器扩展实验+选做实验 | Yesterday17's Blog 新版 GCC 针对 -fdump-translation-unit 的替代方案 | Yesterday17's Blog 一次 HSTS 策略配置的排错之旅 | Yesterday17's Blog YukiNative 踩坑记——Windows 的消息队列 | Yesterday17's Blog 我是我自己——论获取 HTTPS 证书时的验证步骤 | Yesterday17's Blog 【设计文档】对 PUG 的大规模设计修订(1.1) | Yesterday17's Blog GS65 折腾记(2)加装固态,分区,Grub2 引导 Manjaro LiveCD | Yesterday17's Blog 「さくら、もゆ。」的空白字体列表——一次逆向问题定位过程实录 | Yesterday17's Blog GSuite 探索篇(1)使用 Service Account 向 Google Drive 传输文件 | Yesterday17's Blog 『サクラノ詩 -櫻の森の上を舞う-』通关感想 | Yesterday17's Blog 《ATRI -My Dear Moments-》通关感想 | Yesterday17's Blog [工具][VSCode 扩展] AegiKit——方便 Aegisub 使用的工具箱 | Yesterday17's Blog 贝塞尔曲线、字体矢量化与曲线运算 | Yesterday17's Blog
表达式没有应用卡拉 OK 模板会发生什么?——认识 \t 标签 | Yesterday17's Blog
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 这个平时非常常用但却似乎不大了解的特效标签,也算是有所收获吧(

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