特效的设计规律: 强度、时间与结构怎么安排
Note
前面几篇讲的是素材从哪来. 这篇讲素材齐了之后, 那些数字怎么定 (而且每一个都能测.)
那三段素材来自本地成品, 各截 8 秒. 播放器是站点自己在用的 SubtitlesOctopus, 与博客播放器同一套.
0x00 段落内的粒子数是常量, 与句长和字数都无关
有一份 42872 事件的成品, 每次爆发撒多少粒子, 取 {50, 60, 70, 90, 110, 130, 150} 七个档位之一. 数据来自对它 383 次爆发的逐句粒子计数, 并与句长、字数、字宽交叉比对过 (都不成比例). 关键是按段落整段不变, 而且与句长、字数、字宽完全无关.
它的全曲包络是: 60 → 110 → 50 → 70 → 90 → 130 → 150, 分别对应主歌、副歌、内省、过渡、推进、终副歌、收束.
其中 110 掉到 50 那个骤降, 正落在歌词最平静最内省的那一段. 也就是说, 画面在唱到那句的时候把气力抽走了.
这份成品全片只用 6 个图元, 重复了三万多次, 但不显机械. 原因就在这条包络: 同样的笔画被七个能量档位切成了七段.
"不重复"的来源是强度变化, 而不是形状多样性.
0x01 强度: 两个尺度的旋钮, 不是同一个
上面那条是段落级的. 还有一条句级的旋钮, 直觉上容易和它混在一起.
直觉做法是"每个字给固定数量的粒子", 但那样会让长短句的观感差出几倍. 实测发现成品的做法相反: 快嘴句每字少给, 慢句每字多给. 一份成品里, 4 个音节的句子配 79 个事件, 14 个音节的句子配 126 个. 音节比是 1 比 3.5, 事件比只有 1 比 1.6.
结果是每句的总视觉负荷落在 29 到 275 个事件之间, 不随句长线性膨胀.
所以这是两个尺度: 段落级管"这一段整体更浓", 句级管"这一段内长短句拉平". 这两条互不替代. 缺句级补偿, 长句会按字数等比膨胀; 缺段落级包络, 全片会显得平均.
段间的落差可以很大. 天使の3P 那份, 副歌是 464 到 499 事件每秒, 间奏是 0.8 事件每秒, 落差 62 倍; 三段副歌占全片事件的 92.5%. 点兔那份三段副歌 (60 到 80 秒 / 140 到 160 秒 / 220 到 240 秒) 合计 69.8%, 峰值 4502 事件每秒.
而东方的密度档位是 22 个, 从 50 到 192.9 粒每字, 而且是分段常量而不是连续曲线.
0x02 强度跟音量没有关系
这一点值得单独说, 因为最容易搞反.
东方的音频能量峰值在中段, 而粒子密度的峰值在结尾句. 同一句歌词放在主歌是 50 颗, 放在尾声是 165 颗, 差 3.3 倍. 如果跟随响度, 那段反而会变小.
所以密度是人一句一句配出来的, 而不是从音频特征里推出来的.
对照很直接: 音频能量峰值在中段, 粒子密度峰值在结尾句. 两个峰值不在同一处, 就说明两件事各自独立. 这是四份成品的逐秒、逐段事件密度统计与音频能量曲线的对照结果.
0x03 时间: 一次触发挂好几个不同寿命的效果
两份独立拆解交叉验证出同一个结构: 一个字被唱到的瞬间, 会同时触发四到五个效果, 而它们的生命周期完全不同.
字被唱到那一瞬间, 屏幕上同时发生五件事: 整行幽灵层先铺出来 (未唱的字压暗, 一直挂到整行结束), 然后是 500 毫秒的白闪放大回弹、400 毫秒的四角星、800 毫秒的环境粒子穿过去. 唯一不守规矩的是环层, 短则 619 毫秒、长到 1708 毫秒.
母题种类是空间维度的多样性, 时间尺度分层是时间维度的. 后者更难被察觉, 也更有效.
还有一条更细的: 环的参数随字在句中的位置递变. 同一句里越靠后的字, 环存活越久 (619 → 696 → 828 → 1103 → 1708 毫秒), 终态也越大 (232 → 244 → 262 → 280). 这条与前面那条段落级包络是两个不同维度的去机械感手段.
0x04 同一种观感, 三种完全不同的机制
这一节要记住一件事: 看起来一样的"粒子", 实现方式可以完全不同, 成本也差很远.
一份成品的 24530 颗粒子 (占 79.7%) 全落在 y 650 到 700 这一条 50 像素的带里, 而且全部静止, 只靠 t1 错开浮现时刻. 这是对它 30777 颗粒子的 move 参数统计与 y 坐标分布. 观众看到的是"一片星尘缓缓亮起", 而每一颗自始至终都在原地.
对照另一份成品, 它的 \move 共 30328 次, 其中 28113 次写成 \move(X,Y,X,Y,t1,t2) (起点与终点完全相同. 它也在逐帧打印静止粒子.)
为什么会这样? 因为 ASS 没有"沿路径运动"的原生语法. 想做出一颗光点沿线飞过的观感, 只能在每个采样位置各放一颗静止的、彼此错开时间的点, 靠视觉暂留连成线. 生成端的写法 (从模板源码 Vector_Move() 读出) 是把一条贝塞尔曲线按弧长采样成 m 个点, 每点写一条 \move, 用 maxloop(m) 展开成 m 条事件, 残影时长塞进 t1/t2 做交叉溶解.
代价很直观: 一条 400 像素的曲线按 3 像素间隔采样就是 133 条事件, 每条约 309 字节. 一份成品的单句 (10 个字) 派生出了 1217 条粒子事件. 这些事件 100% 带 \alpha&H88&, 集体半透明是为了防过曝.
所以看起来一样的粒子有三种来路: 真在位移的 (东方)、沿曲线采样成静止点靠时间错开连成轨迹的 (羽ばたきのバースデイ)、以及根本不构成轨迹只靠 t1 错开浮现的 (INNOCENT BLUE). 三者都能跑出粒子的观感, 但成本与可控性差很远.
0x05 节奏: 留白是另一个旋钮
有一份成品的间奏段只有 0.8 事件每秒, 帧图近乎纯背景. 另一份更彻底: 落在句外的粒子 0 条, 两段间奏共 38.8 秒的粒子像素实测全为 0.
38.8 秒的静默, 让后面两次副歌有地方可爆. "哪里不放"和"哪里放"一样是设计.
还有一个更少被注意的旋钮. 实测一份成品, 把画面密度归一化到"有内容的帧"之后, 各段是 1153 到 1283 像素每帧, 波动只有 11%, 几乎是常数. 但字幕出现率在各段之间是 52.2% 到 81.3%.
也就是说, 那份成品的副歌是"空气更多"而不是更华丽. 这一段起伏由有字与无字的时间比例制造, 是强度包络之外的另一个旋钮. 数据来自对它 2320 帧的逐段画面密度与字幕覆盖率交叉统计.
这跟强度包络是两个不同的旋钮: 强度包络管有字的时候撒多少, 出现率管有多少时间是空的.
0x06 结构: 用互质的周期把成本和观感解耦
有一份成品的结构很干净: 颜色按行号 mod 3 循环, 动画按行号 mod 4 循环, 33 行里严格符合的有 33 行.
3 与 4 互质, 最小公倍数是 12 行. 相隔 12 行的 (颜色, 动画, 形状) 三元组完全相同, 实测 21 组全同.
成本只有 12 个槽位, 覆盖全片 33 句.
0x07 词意驱动最硬的形态是段落级独占
"唱到花就出现花瓣"这种词级联动听起来直观, 但实测里更常见也更硬的是段落级独占: 某个母题只出现在某一段, 而那段的歌词正好在讲它.
下面三个实例出自同一份成品. 第一, 「四角い箱で息をする」(在方形的箱子里苟延残喘) 这句所在的段落, 方块母题 261 条 100% 只在主歌 1, 其余 6 段一条都没有. 而且方块是从字背后冒出、向上飘、淡出, 花瓣是从字下方升起 (方块是"包裹字的", 花瓣是"从字里落下的".)
第二, 「モノクロの服を着せられて」(被迫穿上黑白制服). 那一整段是全片唯一 PrimaryColour 非白的样式 (#C8C8C8 灰), 粒子调色板是四档灰. 歌里说的"黑白", 被做成了"字幕本身就是灰的".
第三, 「キミはうなづいて泣いた」里的「泣」字, 扩散窗口被拉长到 560 毫秒 (同句其他字是 260 与 350 毫秒), 而且是全片唯一一次"半径 128% 的糊化扩散". 同一个字被刻意放慢 (560 对同句的 350).
判据是机械的: 统计每个母题在段落之间的分布. 段落级独占比词级联动更好验证.
再往上一层是音乐结构驱动. 在 INNOCENT BLUE 那份上实测: 凡有超过 1 秒拖长音节的句子, 紧随的必定是情绪词之一, 而这些词的窗口事件数从平时的 30 到 100 暴增到 2200 到 2600, 二十倍以上. 三处实测是「自由」(拖长 1.28 与 1.13 秒)、「ヒカリ」(1.21 与 1.53 秒)、「ソラ」(1.02 与 1.88 秒).
反例同样干净: 同一个词「ソラ」在没有拖长的两处只有 43 与 29 个事件. 同一个词, 拖长就炸, 不拖长就静, 触发条件是音乐本身. 判据是机械的: 找出所有单字时长大于 1 秒的位置, 查其后 3 字内有没有情绪词, 且该窗口的事件密度是否达到基线的 20 倍以上.
还有一条同构手法: 副歌第一个字被单独踢出来做 0.3 秒的孤立特写 (整屏只剩一个字), 然后才铺开整句.
0x08 粒子不是装饰, 它跟字的点亮是同一个时刻
这一条推翻了一个直觉. 实测东方《果てなき風の軌跡さえ》(风祝): 383 个唯一爆发时刻与 383 个唯一字点亮时刻, 交集是 383 (完全一致, 一个不多一个不少.)
每字爆发 50 到 150 条粒子, 中位 90. 头 50 毫秒先往中间凝聚, 50 到 250 毫秒转白并裂变, 250 到 450 毫秒 blur 增到 6 开始拖散, 到 670 毫秒湮灭.
源点位置固定: y 恒为 880, 源 x 比该字的 x 域各外扩 150 像素; 方向 360 度均匀分布 (24 个 15 度桶的计数起伏只有 ±18%); 位移中位 44 像素.
可以直接照做的生成算法: 对每个字以其 x 为中心左右各扩 150 像素取源区, y 固定, 按均匀方向随机发射 50 到 150 条短命粒子.
0x09 8352 个 \\t 里没有一条加速度曲线
实测 Charlotte 那份: 8352 个 \t 里没有任何一个带加速度曲线, 全部是 3 段线性插值; 而且 \fr、\frz、\fry、\frx、\org 出现 0 次. 它只有 6 种手法, 反复用了 306 秒.
它靠的是两件事: 条带几何, 以及恒定生长速率.
其中一条量化规律可以直接抄: 光环的目标倍率 = 130.46 + 0.1637 × 持续毫秒, 封顶 280. 未封顶样本有 512 个, 残差最大 0.44、标准差 0.26. 作者实际上只定了环活多久, 目标倍率是它推出来的, 所以所有环的角速度看起来一致, 像同一波涟漪.
另一条: 入场与出场的方向正交. 飞入用 \fscx240 横向撕开, 崩出用 \fscy240 纵向拉长.
0x10 呼应只覆盖 6.4 秒: 一个手法值多少的删减测法
把一份成品的画面与它的手法逐段对照, 结果很硬: 画面里最该用"碎裂"手法的三个时刻, 只有第一个真的用了.
t=10.6 秒画面里钟表盘被 8 条白色放射裂纹从中心爆开, 成品在这段用了"字切 4 横条加 4 竖条"的网格化碎裂, 吻合度很高. 但 t=34 秒画面是上下镜像的城市, t=56.5 到 60 秒是红色网格崩坏空间. 这两处最该用碎裂, 成品却回到了常规的"缩小到放大加淡入", 跟别处一模一样.
量化一下: 呼应只覆盖 6.4 秒, 全片 306 秒, 约 2% (按歌词段计是 7%). 做了一个花活, 然后回到流水线.
配色也一样. 那份成品 5 种颜色与画面主色的关系实测基本不存在: 有一种颜色 (#45C043 绿) 全片找不到绿色像素 (19 段里最高只有 0.05%), 却是第三高频的描边色, 用了 1092 次. 其余几种是"最匹配的段"与"实际使用最多的段"对不上.
判定是: 它是配色方案而不是取色方案, 颜色按固定顺序轮换, 每段分到什么就是什么, 不与画面协商. 这本身是一种常见做法, 跟呼应画面与否是两件事; 唯一有画面依据的弱规则是"白描边只在过曝段独占出现".
那么"一个手法到底值多少"能不能测? 能. 做法是复制源文件, 逐个删掉某类事件, 重新渲染同一帧, 比较同帧像素数与文件体积.
"""消融对照: 逐个删掉某类事件后重渲染, 比较同帧像素数。
用它回答"这个手法到底值多少", 而不是靠审美判断。
注意: 纯时间域手法(如 33ms 弹跳)在这里测出来是 0,
它在单帧上不存在, 必须改用连续抽帧做相邻帧差分。"""
import subprocess
import numpy as np
from PIL import Image
def render(ass, t, out="f.png", bg="0x101018"):
subprocess.run([
"ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
"-f", "lavfi", "-i", f"color=c={bg}:s=1920x1080:d=400:r=25",
"-vf", f"subtitles={ass}", "-ss", str(t), "-frames:v", "1", out,
], check=True)
def nonbg(out="f.png", bg=(0x10, 0x10, 0x18), thr=20):
"""同帧非背景像素数 消融比较的度量。"""
a = np.asarray(Image.open(out).convert("RGB")).astype(np.int16)
return int((np.abs(a - np.array(bg)).sum(2) > thr).sum())
# 用法: 原文件与"删掉某一类事件"的文件各渲染同一帧, 比 nonbg 的差值
大切がきこえる 那份做过这个实验:
闪光那条最反常: 只砍 566 行、同帧像素少 1.1%, 全片唯一的高光点就没了. 花海反过来, 砍 835 行掉 40.1% 像素, 副歌和主歌直接分不出来. 中文层砍 50 行掉 15.3%. 33 毫秒弹跳砍 104 行, 单帧一点没变. 翅膀砍 126 行, 像素变化测不出它承载的角色指涉.
最后两条最值得记: 面积占比与观感权重不是一回事; 而且有一类手法在静态帧上完全测不出来 (它只存在于时间维度.)
0x11 因此, 抽帧验收有系统性盲区
上面那个"删掉 33 毫秒弹跳后单帧 0 变化"的结果, 直接推出一个工程结论: 任何"抽 N 帧看完就交付"的流程, 都会漏掉纯时间域的手法.
同一件事的正面证据是同点残影法: 一条曲线被拆成 133 条静止事件, 在任一单帧上只呈现为若干静止点.
补法是: 对同一秒连续抽 10 帧以上做相邻帧差分, 看差分的方差. 只看关键帧不够.
0x12 最后一个统计陷阱
只扫 \1c 会得出"全片无颜色标签"的假结论, 因为有些成品的颜色写在 \1c 与 \3c 之外的不常见槽位组合里. 正确做法是扫描全部 \[1-4]?c 槽位.
那个反例本身就是个设计: 它的字身是 #030303 近黑, 描边是 #B7B7B8 浅灰, 渲染后字身内部 RGB (0,0,0)、描边最亮 (189,189,189). 这是"黑字加亮描边"的负片设计, 与本项目其余成品"白字加暗描边"的路子相反. 代价是不适合压在暗背景上, 收益是极强的机器感. 而那份成品所属的作品正以 CRT 显示器与世界线变动率仪表为核心视觉.
0x13 四组旋钮要按时间窗口排成一个阶梯
这一节是归纳, 不是实测: 事实源里没有现成的阶梯, 是我把几组参数的窗口长度排出来的.
上面讲的几组参数, 按时间窗口从短到长排是这样: 音节级 (拖长就爆发的触发条件) < 字级 (一次触发挂几个效果) < 句级 (句长补偿) < 段级 (强度包络) < 全曲级 (字幕出现率).
相邻两层不该互相补偿. 举例: 如果某一段整体加强了, 同时这段里每句又都变弱, 两个尺度就抵消掉了, 白调. 检查办法是对每层逐个量值, 看相邻两层是否呈负相关; 负相关就说明发生了跨尺度抵消.
0x14 哪些能复用, 哪些不能
同样是参数, 可复用性差得很远.
这一节同样是归纳, 只有前半的循环与包络两条是实测.
12 行超级循环是可复用的: 成本只有 12 个槽位, 与歌词内容无关, 换一首歌照样能用.
强度包络是不可复用的: 每段档位要对歌词内容 (那个 110 掉到 50 的骤降, 正对最内省的段落). 换一首歌, 档位和情绪就对不上了.
两者在同一份成品里并存, 说明可复用的结构与一次性的编排可以分工. 检查办法: 把一份成品的强度包络套到另一首歌上, 看档位与情绪是否还对得上. 对不上, 就说明这类作品只能做一次性的, 做不成模板.
这几组旋钮 (强度、时间、留白、结构) 加上一套消融对照的验法, 基本覆盖了"素材齐了之后怎么调". 它们的共同点是都能用数字验证, 而不是靠看着顺眼.
本文没有引用站内其它文档。

