一帧画面是怎么来的

UNIT 09动画约 34 分钟 · 核心线

动捕的动作为什么套上去就不对?

投篮课第一次把动作库里的一段罚球直接套到教练身上,渲出来是下面这样:两手举过头顶,双脚并拢,手里没有球 C45。MotionLib 的《醒来》里,女主「走路」那一镜,机位整段落在她的头里 C41。《拾光》换上 MetaHuman 以后,主角的胯掉到膝盖那么高,白 T 恤一直拖到地上,像穿了条长裙 C35。三回的起点是同一个念头:动作是现成的,套上去就行。这一课把「套」这一步拆开看。

六格并排:穿白色卫衣的动画角色先双手收在胸前半蹲,再双手举过头顶跳起,双脚始终并拢,手里没有球。
动捕原样套上去。动作库里的一段罚球(来自 Truebones 动作包),直接套在投篮课的教练身上,从头到尾取六格。手里没有球,因为动作文件里本来就没有球;肩头还从卫衣里露出了一块皮肤 C45。

09 · 1一段动作里存了什么

打开一段动作文件,里面没有人,也没有衣服,只有一副骨架(armature):几十根骨骼(bone),一根挂着一根——大腿挂在骨盆上,小腿挂在大腿上,手挂在前臂上。每一帧、每根骨头只记一件事:它相对于挂着它的那一根,转了多少。只有最上面的骨盆多记一样:它自己在哪儿。有的骨架在骨盆上面再加一根根骨(root),位置记在哪一根上,看这副骨架是怎么做的。

这样记很省:肩膀一转,整条手臂连手指都跟着走,用不着一根根去改。记下来的每个值叫关键帧(keyframe)。动画师手工摆的动画(行里叫「手 K」)只在几个要紧的时刻记,中间的帧由一条曲线把前后两个值连起来算出来(插值);动捕是把真人的动作每一帧都录下来,换算成每根骨头的转角,等于每一帧都是关键帧。

那皮呢?模型的网格(mesh)自己不会动。每个顶点记着自己听哪几根骨头的、各听几成——这几成叫权重(weight),加起来是 1。骨头一转,顶点按权重跟着挪,这一步叫蒙皮(skinning)。关节附近的顶点同时听好几根骨头,弯得越狠,几根骨头往不同方向拉得越厉害。

再看文件里没有的:没有球,没有地面;里面那副骨架的长短,是录动作的那个人的,不是你的角色的。头发、帽绳这种跟着身体甩的,也不在里面,它们是播放时现算的:每一小步按上一步的状态往前推一点,叫弹簧骨骼(spring bone)。现算的东西有个脾气:要是每一步推多远跟着帧率变,直接跳到第 3 秒和一帧帧放到第 3 秒,走过的路不一样,头发的样子就不一样。投篮课的办法是固定每 1/60 秒推一步,跳到哪一刻,都先从前一秒开始重跑:每次从同一个起点、用同样大的步子走过来,结果就一样了 C46。

关节一弯,皮为什么会瘪

「按权重跟着挪」到底怎么挪?每根骨头都会把这个点带到一个位置:上臂带它,它留在原地;前臂带它,它跟着前臂绕肘转。权重决定在这几个位置之间取哪一点:各听一半,就取两个位置的正中间。这个做法叫线性混合,游戏和 Blender 默认的蒙皮都是它。

上臂不动,前臂往上弯 120°。肘外侧正中那个皮上的点,听上臂、前臂各一半。它会落在哪?

  1. 顺着弯,转过 60°,离肘还是那么远
  2. 落在两个位置的连线中点,离肘近了一半,肘瘪下去
  3. 留在原地不动:上臂那一半把它按住了
  4. 被两根骨头往两边拉,皮从这一点裂开

离肘只剩一半。只听上臂,它在原地;只听前臂,它转过 120°。两个位置离肘都是 5 厘米,可两点连线的中点离肘只有 5 × cos 60° = 2.5 厘米。混合的是位置,不是角度:弯得越狠,这条连线越贴近肘,关节就越瘪(下面的实验台现算)。

猜「转过 60°」的,想的是「转一半的角度」,那是下面实验台 03 项辅助骨的做法;普通的权重只在两个位置之间取点。猜「裂开」的:一个点只有一个位置,裂开是相邻两个点被拉得很远,全给一根骨头时外侧那一段就是。

实验台 D-1皮跟着骨头走
来自材料
蒙皮的做法:每个点的位置 = 各根骨头带它去的位置按权重加权平均,权重加起来是 1(Blender 手册「骨架」一章的蒙皮与权重);C50 的撕开出在胯部顶点的权重。
本实验的设定
侧面看一截 10 厘米粗的「手臂」:上臂 20、前臂 18 厘米,皮上每 2 厘米一个点;过渡带里「听前臂几成」按离肘的距离直线过渡;辅助骨装在肘上,永远只转肘角的一半。这些数是为了看清楚挑的,不是投篮课的模型。
它能证明
每个点落在各根骨头带它去的几个位置之间;全给一根时外侧被削平;过渡带让弯处顺了,肘正中的点却只剩 cos(弯角 ÷ 2);加一根转一半的辅助骨,能撑回大半。
它不能证明
真实角色是三维的:前臂还会拧,衣服和身体是两层网格各自蒙皮,胯部的点同时听好几根骨头;修正形态键要事先雕好形状,这里没做。

把 02 的过渡带拖到 0(全给一根):肘两边的点各跟各的骨头走,外侧中间那段皮被拉成一条直线,把肘削平了。拖宽过渡带,弯处顺了,可肘正中那一点还是各听一半:弯 120° 剩一半厚,弯 150° 只剩四分之一。光调权重,治得了「折」,治不了「瘪」。再挑一个点,把它的权重拖到一头:那一点被拽回上臂、或者被甩到前臂那边,旁边的皮跟着被扯歪。

做角色的人(或者 AI)汇报修法,常见的就三样,听得懂就够了:

C50 的裤裆撕开,是同一件事发生在胯上:裆部那片网格同时听骨盆和两条大腿,腿一张开,相邻的点被拽向不同方向,拉开的地方就露出了底下的皮肤和衬里(当年最后怎么修的,没留下记录)。

同一个角色抱着篮球正面站立两张:左边两脚分开,裤裆处裤子裂开,露出皮肤和红色衬里;右边两脚并拢,裤子完好。
两脚一分开。投篮课用程序把两脚摆开(腿部 IK,下一节讲),分开站时(左),裤裆那里露出了底下的皮肤和红色的衬里;两脚并拢(右)就没事 C50。两边骨架的动作都是对的。

09 · 2把角度原样抄过去,手会落在哪?

把一段动作从一副骨架搬到另一副上,叫重定向(retargeting)。最省事的重定向,是按名字把骨头一根根对上,每一帧的转角原样抄过去。两副骨架一模一样时,这就够了。可动作库里的人和你的角色,胳膊不会一样长。

拿投篮课的教练当动作的主人。他是 VRoid 的样例角色 HairSample_Male;从模型文件里量,上臂 22.0 厘米、前臂 24.9 厘米。他托球时上臂往前、比水平抬高约 10°(本课的设定),肘弯成 86°(投篮课从渲出来的画面上量过,本节末讲怎么量 C45),手正好在球下面。现在把这两个角原样抄给另一个 VRoid 样例角色 Vivi:上臂 18.6、前臂 18.0 厘米。

肩、肘两个角原样抄给 Vivi,两人的肩膀放在同一个位置。Vivi 的手还托得住那个球吗?

  1. 托得住:角度一样,姿势就一样
  2. 差一点,不到一厘米,看不出来
  3. 托不住:手落在球下面好几厘米
  4. 托得住:骨架会把前臂拉长,补上差的那一截

差 7.5 厘米,手落在球下面(下面的实验台现算)。手在哪,是从肩膀出发,先沿上臂走一段、再沿前臂走一段走到的:角度只管每段朝哪,长度管走多远。两段都短了三四厘米,手就停在了近一截的地方。骨头也不会自己伸长,那得改模型,放动作办不到。

就算身材差不多也不行:实验台里换成 AvatarSample_B,上臂还比教练长一点,照样差 2.6 厘米。

实验台 D-2拖手
来自材料
三个角色的上臂、前臂长度(从投篮课的 .vrm 模型文件量出);教练托球时肘 86°(C45,量的是渲染画面);两骨 IK 的算法(投篮课 web/src/ik.js,用脚本和原代码逐点对过,肘的位置一致到小数点后十几位)。
本实验的设定
侧面看,只画一条手臂,两人的肩膀放在同一点;教练的上臂比水平抬高 10°;球按 7 号球的大小,摆在教练托球的地方。
它能证明
同样的角度、不同的骨长,手落在不同的地方;手的位置给定以后,肘角由三条边唯一决定,两个解由极向量挑。
它不能证明
真实的投篮是三维的,还有手腕、手指、肩胛在转,肩膀的高度也随身高变——整个人放在一起,差得比这里多(Vivi 的肩比教练低 31 厘米)。

所以不给角度,给手一个位置:「手腕到这儿」,让电脑倒推肩和肘该转多少。这叫反向运动学(inverse kinematics,IK);前面那种给角度、算手在哪的,叫正向运动学(FK)。把实验台 02 换成「给手一个目标」,Vivi 的手就到了球下面。

电脑怎么倒推?手腕定了,肘离肩是上臂那么长,离手腕是前臂那么长,所以它必须同时落在两个圆上:以肩为圆心、半径 L1 的圆,和以手腕为圆心、半径 L2 的圆(实验台里那两个虚线圆)。两个圆交在两个点,肘就在其中一个上。三条边都有了,肘角也就定死了。第三条边 d 是肩到手腕的距离:教练托球时手离肩 32.0 厘米(用他的骨长和两个角算得出来),换成 Vivi 也得这么远。由三边求角,用的是余弦定理 c² = a² + b² − 2ab·cos C,移一下项(arccos 是「已知余弦值,反查角度」):

肘角 = arccos((L1² + L2² − d²) ÷ (2 × L1 × L2))
Vivi:arccos((18.6² + 18.0² − 32.0²) ÷ (2 × 18.6 × 18.0)) ≈ 122°

同一个球,换到 Vivi 身上,肘要张到约 122° 才托得住,教练是 86°。抄过去的角度注定不对,该抄的是「手在球下面」这件事。投篮课的 ArmIK(web/src/ik.js)就是这么倒推的。拖一拖实验台里的十字把手,看三条边和肘角一起变。

把把手往远处拖。超过 L1 + L2,两个圆碰不上了:手臂伸直、指着目标,还差多少报多少,骨头不会变长。拖到离肩太近也不行,两个圆一个套在另一个里面,肘折到底也够不着。

两个交点挑哪个?靠一个叫极向量(pole vector)的点:肘往它那边弯(实验台 03 可以翻过去)。投篮课有个「常见错误:手肘外翻」开关,同一帧里它只动了两样:手腕目标往外挪约 12 厘米,极向量往外拉。

黄昏的球场上,同一个角色出手的同一瞬间,左右两张:左边肘在球的正下方;右边肘向外拐了出去,球也跟着偏了。
同一帧,只动了手腕目标和极向量。左:正常出手,肘在球的正下方;右:打开「手肘外翻」,肘向外拐了出去。投篮课的错误动作开关(web/src/steps/shooter.js)C45。

改完了,怎么知道改对了

IK 是给位置、求角度;反过来也行:从画面里找到位置,量出角度。投篮课改完动作,拿 MediaPipe(谷歌的姿态估计模型,每一帧给出身上 33 个关键点)去看自己渲出来的画面,肩、肘、腕三个点一连,用的还是那条余弦定理:肘 86°、膝 136°,两脚间距约是肩宽的 1.08 倍,和讲稿要的一致 C45。它的跟练功能用的是同一套:在你自己的电脑上分析摄像头画面,不上传,肘在 82° 到 98° 之间算对。量的是三维的角(MediaPipe 还给出每个点的三维坐标),不是画面上连出来的平面角。

球场上一个角色正面举球准备投篮,身上叠着青色圆点:两肩、两肘、双手、两胯、两膝、两脚各有点。
从画面里找关节。MediaPipe 在投篮课渲出来的一帧上找到的关键点(青色圆点)。同一批测试里,纯侧面的那一张一个点也没找到 C45。

09 · 3往前走的那几米,是谁在推?

走路是腿在转,可人往前走的那几米,不是哪根腿骨转出来的。前面说过,只有最上面那根骨头记着自己在哪:走路时,是骨盆一帧帧往前挪,整副骨架被它带着走,腿在下面一步步接住。动作自己带着的这段位移,叫根运动(root motion)。

《醒来》用的 VRoid 角色,走路的位移记在骨盆上,而骨盆那根骨头在 Blender 里被设成了「连接」(Connected)在它上面的根骨上。连接的意思是:这根骨头的起点焊死在它挂着的那根骨头的末端,只能转,不能挪。

这样的角色,播一段往前走 1.5 米的动作,会怎样?

  1. 照样走出 1.5 米:腿的转角一个没少
  2. 原地迈腿,一步也走不出去
  3. 整个人倒下去
  4. 走得出去,只是慢一半

原地踏步 C41。Blender 丢掉了每一帧骨盆的位移,只剩转角照播:腿在迈,身体不走;蹲下、站起也做不出来,身体一直停在站直的高度。那一镜的机位是按她走完的距离摆的,她没走,机位就落进了她的头里。已经交付的《霓虹之夜》动画版也带着这个毛病,舞者的上下起伏和位移都没了,缩略图里看不出来。修法是把 12 个 VRoid 角色骨盆的「连接」关掉、重建,之后测试的走路动作向前走了约 1.5 米。

猜「照样走」的,以为往前走是腿转出来的;猜「倒下去」的,其实腿的转角一样没少,少的只是骨盆那一段位移。

实验台 D-3谁在推
来自材料
走路动作原速每秒 1.40 米、跑步约 2.2 米,路人 0.95–1.4 米、旅人跑 5.2 米、修后 3.3 米(C36);骨盆「连接」在根骨上会丢掉位移(C41);腿长取教练的模型(大腿 38.8、小腿 45.7 厘米)。
本实验的设定
步态是画出来的火柴人:一步多长、一秒几步、脚在地上的时间占多少、脚怎么抬,都是本课定的;腿用两骨 IK 摆到脚上,骨盆的起伏是腿长逼出来的。
它能证明
脚每秒滑多少 = 身体速度 − 播放倍率 × 动作原速;动作自己带着骨盆走时,着地的脚在地上一动不动(探针算过,每帧挪动小于 10⁻¹⁵ 米)。
它不能证明
真实的动捕,脚不会每一帧都这么干净地贴着地;《拾光》用的是 UE 自己的动画系统,不是这个火柴人。

实验台默认就是那个毛病:她在原地迈腿,脚在地上往后蹭,擦出一道道红印子;虚线的那个人,是动作里她本来该在的位置。把 01 换成「根运动(动作自己带着走)」,脚印就一个个留在地上:每只脚踩下去就不再动了。

脚为什么在地上打滑

游戏里常常反过来做:动作做成原地的,骨盆不往前走,角色往哪走、走多快,由游戏自己推。《拾光》的路人就是这样,速度在每秒 0.95 到 1.4 米之间随机 C36。原地的动作里,踩在地上的那只脚每一帧往后退一点,退得多快,就是动作「本来」的速度——录的时候人走多快,它就退多快。《拾光》的走路动作是每秒 1.40 米。

身体被游戏以速度 v 往前推,脚按动作的速度往后退(动作要是被放快或放慢,这个速度也跟着乘上播放倍率),落到地上的,就是两者的差:

脚每秒滑多少 = v − 播放倍率 × 动作原速

旅人跑起来每秒 5.2 米,跑步动作的原速只有约 2.2 米,照原速播。脚踩在地上的那几帧,会怎样?

  1. 不滑,脚一直贴在地上
  2. 往后滑,像踩在跑步机上
  3. 往前滑,每秒约 3 米
  4. 往前滑,每秒约 5 米

每秒往前滑 3.0 米:5.2 − 2.2 = 3.0。脚刚踩下去,就被身体拖着往前溜 C36。

猜「往后滑」的,把方向弄反了:身体比动作快,脚是被往前拖;身体比动作慢(比如每秒 0.95 米的路人配 1.40 的走路动作),脚才往后蹭。猜「每秒约 5 米」的,忘了动作里的脚本来就在往后退。

修法有两个旋钮:让动作播快一点,播放倍率 = 实际速度 ÷ 原速;或者让角色别跑那么快。《拾光》两样都用了:步子快慢跟着实际速度走,旅人跑速降到每秒 3.3 米 C36。按 5.2 算,腿要倒腾到原速的 2.4 倍;降到 3.3,是 1.5 倍。回到上面的实验台 D-3 试一试:01 选「原地动作 + 游戏推」,在「《拾光》的数」里换几组,再打开「播放倍率跟着速度走」。

09 · 4名字对上了,为什么还穿成了长裙?

真正的重定向工具,比按名字抄角度多做好几件事。头一件是对齐静止姿势,也就是每根骨头都没转的时候,这副骨架摆成什么样(比如 T 字、A 字):转角都是从它开始量的,起点不一样,同样的转角摆出来就不一样。MotionLib 去重后有 11,226 条动作,来自好几种骨架:Mixamo、3ds Max 自带的 Biped 骨架、DAZ(另一个做人物的软件)、各种 BVH 动捕文件。名字不同还好办,麻烦在静止姿势:Biped 的静止姿势是随意摆的,得先按身体的解剖方向把每根骨头摆正,再对应过去。最后 11,104 条统一到了 Mixamo 的骨架上,115 条骨架太特殊,没统一成。

再一件是骨盆的高度,和骨骼链(一串连着的骨头,比如一条腿、一根脊椎)怎么配。《拾光》把动作重定向到 MetaHuman 上,主角走起来像跪着,胯在膝盖那么高,白 T 恤拖到地上。先怀疑布料模拟(根本没开),再怀疑骨架(和路人的一样),最后把每根骨头的位置打印出来:骨盆几乎贴地,大腿和脊椎各离骨盆 80 多厘米。毛病在重定向这一步:UE 里专门做重定向的工具,默认开着「根运动」「全身 IK」两步,把骨盆的高度算到了贴地;大腿、脊椎却还在原来的高度,挂在骨盆上的胯和 T 恤下摆被一起拽到地上,就成了长裙。自动给骨骼链配对时,它还把 MetaHuman 的根骨那条链配给了动作库角色的右脚。修法是只留「骨盆运动」「逐根抄转角(FK 链)」两步,解开错配,21 段动作重转;验收写成了数:骨盆离地约 83 厘米,第一节脊椎和左大腿离骨盆都不到 10 厘米 C35。

也不是每个看着别扭的动作都是管线出了错。红魔机甲那段「尬舞」,动作本身没坏,是挑了「挑衅」「拳击待机」这样的表演 C29。投篮课库里的投篮也全是双手过头、双脚并拢 C45,它的办法是身体的节奏用动捕,手臂和脚用 IK 摆到讲稿要的位置——这一课的几样东西,合在一起用。

最后一道,换个方向想。

带走一个问题

把教练整个等比放大到 1.2 倍,每根骨头都长 1.2 倍。用同一段原地走路动作,游戏里还是每秒 1.40 米、原速播放:脚会滑吗,往哪边?他手里那个球,还托得住吗?(先想一想:动作的「原速」是从哪来的。)

不看上面,写下这一课撞上的几堵墙,再展开对照
  1. 骨头对了皮就对了 → 皮上的点在几根骨头带它去的位置之间按权重取,大角度时关节会瘪、会撕开;修法是调权重、加辅助骨、修正形态键。
  2. 把角度原样抄过去 → 胳膊长短不同,手落到别处:给手一个目标,用 IK 倒推角度(两个圆的交点、余弦定理),极向量挑一个解。
  3. 以为腿在转人就在走 → 往前的那几米是骨盆的位移(根运动);骨盆被「连接」住就原地踏步。
  4. 以为播放速度和移动速度没关系 → 脚每秒滑 v − 倍率 × 原速。
  5. 以为名字对上就能重定向 → 还要对齐静止姿势、配对骨骼链、管住骨盆高度,验收写成数。